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

7 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
  5. that's truly awesome, bro. your article about cockfight in philipine make remember about one and only greatest boxer from philipine, manny pacquaio. many issues mention he have big chicken farm in philipine, we are from sabung ayam online wherever called online cockfight agent literally want to talk about this issue, but many people think that's jokes to full detail at www.ayamkari.net.

    you can visit our blog in http://ayambangkokportal.blogspot.com/2018/06/tipe-latihan-untuk-ayam-aduan-yang-akan.html
    but in my blog user bahasa (indonesia means like bali ), if you want to know much about cockfight, you can contact ours.

    ReplyDelete