Tuesday, March 12, 2013

Change Device COM Port via PowerShell

On a recent project, we had a situation where we had to make sure that a specific USB hardware device was always assigned to a COM1.  Because of the way USB devices are assigned COM ports, it was never going to get COM1 the first time. I whipped up a quick script to check for the device and then change its COM port. It worked great, but as is the case with many first iterations of scripts, there was a but...

Even though a new COM port was getting assigned, we found is that the previous COM port was remaining marked as used in the ComDB. The ComDB is a binary registry value that keeps track of which COM ports are in use. A great description of how the value works can be found here: http://www.rosseeld.be/DRO/PIC/NextSerialPortNo.htm.

To solve the problem we had two options:
1) Find a way to get access to the msports.dll functions within PowerShell which likely meant digging into the Windows Driver SDK.
2) Parse the binary registry value and update the ComDB manually.

I chose door number two because it seemed much more straight forward. The task was actually a little bit trickier than I expected because the ComDB reverses the COM port bits in each byte (e.g. COM1 is represented by Bit 8 and COM8 is represented by Bit 1). Also, to properly convert the Byte value, each Byte had to have 8 values (leading zeros can't be truncated). To remedy that I used a loop to make sure the binary character array had the proper number of entries.

The function below is the end result...

$DeviceName = "My Hardware Name"
$ComPort = "COM4"


function Change-ComPort {

Param ($Name,$NewPort)

#Queries WMI for Device
$WMIQuery = 'Select * from Win32_PnPEntity where Description = "' + $Name + '"'
$Device = Get-WmiObject -Query $WMIQuery

#Execute only if device is present
if ($Device) {

#Get current device info
$DeviceKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $Device.DeviceID
$PortKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $Device.DeviceID + "\Device Parameters"
$Port = get-itemproperty -path $PortKey -Name PortName
$OldPort = [convert]::ToInt32(($Port.PortName).Replace("COM",""))

#Set new port and update Friendly Name
$FriendlyName = $Name + " (" + $NewPort + ")"
New-ItemProperty -Path $PortKey -Name "PortName" -PropertyType String -Value $NewPort -Force
New-ItemProperty -Path $DeviceKey -Name "FriendlyName" -PropertyType String -Value $FriendlyName -Force

#Release Previous Com Port from ComDB
$Byte = ($OldPort - ($OldPort % 8))/8
$Bit = 8 - ($OldPort % 8)
if ($Bit -eq 8) { 
$Bit = 0 
$Byte = $Byte - 1
}
$ComDB = get-itemproperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\COM Name Arbiter" -Name ComDB
$ComBinaryArray = ([convert]::ToString($ComDB.ComDB[$Byte],2)).ToCharArray()
while ($ComBinaryArray.Length -ne 8) {
$ComBinaryArray = ,"0" + $ComBinaryArray
}
$ComBinaryArray[$Bit] = "0"
$ComBinary = [string]::Join("",$ComBinaryArray)
$ComDB.ComDB[$Byte] = [convert]::ToInt32($ComBinary,2)
Set-ItemProperty -path "HKLM:\SYSTEM\CurrentControlSet\Control\COM Name Arbiter" -Name ComDB -Value ([byte[]]$ComDB.ComDB)

}
}

Change-ComPort $DeviceName $ComPort