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

6 comments:

  1. Hey Jeremy, I'm going through the same thing right now and was stoked to see that someone already wrote a script.

    I am getting an error though. My signature pad is set to COM4 and I need change it to COM1.

    I set your vars to:
    $DeviceName = "Keyspan USB Serial Port"
    $ComPort = "COM4"

    When I run the script here is the output. Do you have any advice? Thanks so much in advance!

    PS C:\users\squirion\scripts> Change-ComPort $DeviceName $ComPort


    PortName : COM1
    PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\KEYSPAN\*USA19HMAP\00_00\Device Parameters
    PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\KEYSPAN\*USA19HMAP\00_00
    PSChildName : Device Parameters
    PSDrive : HKLM
    PSProvider : Microsoft.PowerShell.Core\Registry

    New-ItemProperty : Requested registry access is not allowed.
    At line:25 char:1
    + New-ItemProperty -Path $DeviceKey -Name "FriendlyName" -PropertyType String -Val ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : PermissionDenied: (HKEY_LOCAL_MACH...USA19HMAP\00_00:String) [New-ItemProperty], SecurityException
    + FullyQualifiedErrorId : System.Security.SecurityException,Microsoft.PowerShell.Commands.NewItemPropertyCommand

    ReplyDelete
  2. I found out, that you have to run this script with system rights, thanks to this post:
    http://stackoverflow.com/questions/6535644/change-port-permanently-using-wmi-and-powershell

    I run it as a scheduled task where I can do that, to run a script just to from the powershell prompt I don't know really how, but maybe google can help :)

    Thanks Jeremy for the great script!

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Instead of FriendlyName, which might not be very friendly, I prefer to map the COM* to the DeviceID:s

    # Get available COM devices
    Add-Log "Scanning COM-ports..." -Level 1
    [array]$COM = Get-WMIObject Win32_PnPEntity | where {$_.Name -like "*(COM*"}
    foreach ($port in $COM) {$port.Caption = $port.Name.Split("(")[1].Trim(")")} # Borrow Caption for COM-number
    $COM = $COM | Sort-Object Caption

    $COMlist = New-Object System.Collections.Specialized.OrderedDictionary # Use ordered list instead of messy hash table
    foreach ($port in $COM)
    {
    $COMlist.Add($port.Caption,$port.DeviceID)
    Add-Log ($port.Caption + " " + $port.DeviceID) -Level 4
    }

    And then use i.e. $COMlist["COM2"] with the function.

    function Set-ComPort
    {

    Param (
    [string]$DeviceId,
    [ValidateScript({$_ -clike "COM*"})]
    [string]$ComPort
    )

    #Queries WMI for Device
    $Device = Get-WMIObject Win32_PnPEntity | where {$_.DeviceID -eq $DeviceId}

    #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 = $device.Name.split("(")[0] + "(" + $ComPort + ")"
    New-ItemProperty -Path $PortKey -Name "PortName" -PropertyType String -Value $ComPort -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
    } # end if

    $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
    } # end while

    $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)

    } # end if
    } # end function Set-ComPort()

    Thus, the usage for changing COM2 to COM8 would be

    Set-ComPort -DeviceID $COMlist["COM2"] -ComPort "COM8"

    ReplyDelete
    Replies
    1. Forgot to remove the line Add-Log. It's an internal function.

      Delete