serial ports on Windows

I created this page in case some people may have some questions or answers about using a serial port on Windows.

I just tried to communicate with com1 on a Windows 2000 machine. For some reason I could not receive any data using the following commands in Wish.

   % proc rd_chid {chid} {
   set msg [gets $chid]
   puts $msg
   }
   % set com [open com1: r+]
   % fconfigure $com -mode 9600,n,8,1 -blocking 0 -translation auto -buffering line
   % fileevent $com readable [list rd_chid $com]

However, I was able to receive data by putting those last 3 lines into its own procedure...

   % proc rd_chid {chid} {
   set msg [gets $chid]
   puts $msg
   }
   % proc open_com {} {
   global com
   set com [open com1: r+]
   fconfigure $com -mode 9600,n,8,1 -blocking 0 -translation auto -buffering line
   fileevent $com readable [list rd_chid $com]
   }
   % open_com
   # lots of data comes streaming :)
   % close $com

jg


Tcl 8.4 greatly improved serial port control across platforms. Refer to the updated fconfigure documentation for more info.


To retrieve details on input/output errors--which are, in general, frequent, when working with serial lines--interrogate

    set details [fconfigure $serial_handle -lasterror]

8.3.4 introduced this capability.


How can I connect to a com port higher than 9? (I am using a terminal server, and its driver creates com ports upto 16 or so.) Would appreciate answer by mail - [email protected]

Thanks

25Aug2003 PS: You need to use the 'generic' name (I think it is actually the UNC path) of the com port. Use the file name \\.\com13 to open 'com13:' Mind you, windows wants to see those all those \ chars, so use [open \\\\.\\com13 r+]. I have not tested this myself... E_NO_ACCESS_TO_BIG_SERIAL_CARD. But it does work :)

AMG: You can use {brace quoting} to eliminate the need for doubled \backslashes.

20111014 tested with 8.5.9: open //./com13 definitely works (or \\\\.\\com13), shorter variants don't. Interesting detail: if you are using a tclkit app, you can open com5 etc only if you have the current directory NOT within tclkit VFS, but outside of this (com1...4 always open regardless current dir). But using the absolute path as proposed above, VFS or normal current directory does not matter.


The following code will enumerate the serial devices on a windows XP machine - including USB devices.

 package require registry
 
 proc get_serial_ports { } {
    set serial_base "HKEY_LOCAL_MACHINE\\HARDWARE\\DEVICEMAP\\SERIALCOMM"
    set values [ registry values $serial_base ]
 
    set result {}
 
    foreach valueName $values {
       lappend result [ registry get $serial_base $valueName ]
    }
 
    return $result
 }

Yep, the above works fine on XP. Try " return [lsort -dictionary $result]" to get the list sorted... -regards

ZB 20111012 - yes, it works on my Win XP - but I had the problem, that id did list also the "virtual serial ports" left by Bluetooth device (not connected). Trying to open such serial port resulted in very long pause (almost a minute), before "open" gave up.


enumerate serial devices on win2k-win7(Jee Labs version [L1 ])

# 2010-04-21 improved version, see http://talk.jeelabs.net/topic/208
# 2012-03-16 further improved, added detection for Brainboxes and ACPI

#
#  List serial ports
#
#  Identifies the following devices:
#    ftdi, acpi, usb, bbx (Brainbox)
#
#  Resulting list:
#    <prefix>-<serial> COM<xy>
##
proc RawListSerialPorts {} {
    set result {}
    set ccs {HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet}
    foreach {type prefix match} {
        Serenum  ftdi {^FTDIBUS.*_6001.(\w+)}
        Serenum  acpi {^ACPI.*(\w+)}
        usbser   usb  {^USB\B.*\B(.*)$}
        BbEnum20 bbx  {^Ports\\BbUSer([0-9]*).*}
    } {
        # ignore registry access errors
        catch {
            set enum "$ccs\\Services\\$type\\Enum"
            set n [registry get $enum Count]
            for {set i 0} {$i < $n} {incr i} {
                set desc [registry get $enum $i]
                if {[regexp $match $desc - serial]} {
                    set p [registry get "$ccs\\Enum\\$desc\\Device Parameters" PortName]
                    # Log . {usb-$serial Port: $p\
                                   Friendly: [registry get "$ccs\\Enum\\$desc" FriendlyName]}
                    lappend result $prefix-$serial $p
                }
            }
        }
    }
    return $result
}

Note that you most frequently see code something like this:

 set serial [open "COM1:" "RDWR"]
 fconfigure $serial -mode 9600,n,8,1

where fconfigure sets the baud rate, etc. of the serial port.

ZB The above procedure didn't work at all on my Win XP.

PY As for me, I use next snippet to obtain virtual COM ports:

# List all ports as FTDIBUS and USB. Returns list of {PortName, FriendlyName, Auto?}
# where Auto is flag - port for HMC6343 or not
proc ls_virt {args} {
    set res ""
    foreach {type vid} {FTDIBUS VID_* USB V[iI][dD]_*} {
        set k0 "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\$type"
        catch {  ;# catch case where e.g. FTDIBUS is not found
            set k0ch [registry keys "$k0" $vid]
            foreach k1 $k0ch {
                set found [regexp -nocase $args $k1]
                set k1ch ""
                catch {set k1ch [registry keys "$k0\\$k1"]}
                foreach k2 $k1ch {
                    set Class ""
                    catch { ;# nested catch to prevent total loop break
                        set Class [registry get "$k0\\$k1\\$k2" Class]
                        if {$Class == "Ports"} {
                            # only when Class is defined, the driver is truly installed
                            set FriendlyName [registry get "$k0\\$k1\\$k2" FriendlyName]
                            if {$found || [regexp -nocase $args $FriendlyName]} {
                                set PortName [registry get "$k0\\$k1\\$k2\\Device Parameters" PortName]
                                set Service [registry get "$k0\\$k1\\$k2" Service]
                                lappend res [list $PortName $FriendlyName $Service]
                            }
                        }
                    }
                }
            }
        }
    }
    return $res
}

RJM extended by scanning not only FTDI-based ports and case-tolerant VID/Vid string recognition as well as added robustness. Further, a result filter may be specified, used to filter according to FriendlyName substring or Pid&Vid key

Third item is a flag - is it our device (I'm looking for HMC6343 sensor, but it's possible to test instead of silabser any other driver/service type).


_abc_ - 2012-01-27 14:56:35

code to enumerate all serial ports, cross platform, tested to work well on various linuxes and windows xp and windows 7. Contributed by emiliano and tested by me on several machines.

package require platform

namespace eval serial {
        variable platform   [lindex [split [platform::generic] -] 0]
}

  proc serial::listports {} {
        variable platform

        set result {}
        switch -- $platform {
                win32 {
                        catch {
                                package require registry
                                set serial_base [join {
                                        HKEY_LOCAL_MACHINE
                                        HARDWARE
                                        DEVICEMAP
                                        SERIALCOMM} \\]
                                set values [ registry values $serial_base ]
                                foreach value $values {
                                        lappend result \\\\.\\[registry get $serial_base $value]
                                }
                        }
                }
                linux {
                        set result [glob -nocomplain {/dev/ttyS[0-9]} {/dev/ttyUSB[0-9]} {/dev/ttyACM[0-9]}]
                }
                netbsd {
                        set result [glob -nocomplain {/dev/tty0[0-9]} {/dev/ttyU[0-9]}]
                }
                openbsd {
                        set result [glob -nocomplain {/dev/tty0[0-9]} {/dev/ttyU[0-9]}]
                }
                freebsd {
                        # todo
                }
                default {
                        # shouldn't happen
                }
        }

        return [lsort $result]
 }

aspenlogic 2014-04-30

All of the scripts above worked to locate the names of my Windows 7 (64-bit) COM ports. However, a simple [open COM2: r+] always failed. On a hunch I tried running wish with Administrator privileges. After that, open worked just fine.


HE 2014-07-13: I had the task to show the currently existing devices connected via serial interfaces and show them with their friendly name. The solution I found is a combination of the above mentioned registry keys. It provides a list of list with two values (COM port and friendly name):

proc getSerialInterfaces {} {
        set coms {}
        set res  {}
        set key "HKEY_LOCAL_MACHINE\\HARDWARE\\DEVICEMAP\\SERIALCOMM"
        if {[catch {
                # The keys only available if the driver is loaded.
                foreach name [registry values $key] {
                        set value [registry get $key $name]
                        lappend coms $value
                }
        } fault]} {
                # return a empty list in case no serial interface is found
                return $res
        }

        set key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum
        foreach el [registry keys $key] {
                foreach el1 [registry keys $key\\$el] {
                        catch {
                                foreach el2 [registry keys $key\\$el\\$el1] {
                                        set port [registry get "$key\\$el\\$el1\\$el2\\Device Parameters" PortName]
                                        if {[lsearch $coms $port] < 0} {
                                                continue
                                        }
                                        set friendlyName [registry get "$key\\$el\\$el1\\$el2" FriendlyName]
                                        lappend res [list $port $friendlyName]
                                }
                        }
                }
        }
        return [lsort $res]
}

KL 2017-11-10 NOTE: This implementation does not work in WIN 10 can somebody provide a solution ??

JRP 2020-6-4: Here are some procs I use to find serial ports on Windows 10 using TWAPI. I use the vendor and product IDs to find specific devices.

package require twapi
package require textutil

proc get_windows_hwid_value {hwid delimiter} {
    # Return a value from a windows hardware ID
    #
    # Arguments:
    #   hwid -- Windows hardware ID, like USB\VID_1FFB&PID_00BB&REV_0102&MI_01
    #   delimiter -- Substring like "VID_" or "PID_"

    # Prefix string will start with the value we want terminated in a &
    set prefix_string [lindex [textutil::splitx $hwid $delimiter] 1]
    set value [string trimleft [lindex [split $prefix_string &] 0] 0]
    return $value
}

proc detect_vcps {} {
    # Windows -- returns a dictionary of:
    #
    # (COM port): vendor_id, product_id, composite_index
    set vcp_dict [dict create]
    switch $::tcl_platform(os) {
        {Linux} {
            set nodelist ""
            try {
                # FTDI devices show up as ttyUSB*
                set nodelist [glob -nocomplain -directory /dev/ ttyUSB*]
                # Arduino devices show up as ttyACM*
                lappend nodelist [glob -nocomplain -directory /dev/ ttyACM*]
            } trap {} {message optdict} {
                puts $message
                set nodelist ""
            }
            foreach vcp [join $nodelist] {
                try {
                    set udev_data [exec udevadm info --query=property --name $vcp]
                    set id_serial ""
                    set id_usb_interface_number ""
                    foreach line [split $udev_data "\n"] {
                        if {[string first "ID_SERIAL=" $line] >= 0} {
                            set id_serial [lindex [split $line "= \n"] 1]
                        }
                        if {[string first "ID_USB_INTERFACE_NUM=" $line] >= 0} {
                            set composite_index [string trimleft [lindex [split $line "= \n"] 1] 0]
                        }
                        if {[string first "ID_VENDOR_ID=" $line] >= 0} {
                            set vendor_id [string trimleft [lindex [split $line "= \n"] 1] 0]
                        }
                        if {[string first "ID_MODEL_ID=" $line] >= 0} {
                            set product_id [string trimleft [lindex [split $line "= \n"] 1] 0]
                        }
                        

                    }
                } trap CHILDSTATUS {results options} {
                    puts "oops -- $results"
                    set status [lindex [dict get $options -errorcode] 2]
                    continue
                }
                puts "ID_SERIAL is $id_serial"
                puts "  Vendor ID: $vendor_id"
                puts "  Product ID: $product_id"
                puts "  COM port: $vcp"
                puts "  Composite index: $composite_index"
                dict set vcp_dict $vcp vendor_id $vendor_id
                dict set vcp_dict $vcp product_id $product_id
                dict set vcp_dict $vcp composite_index $composite_index
            }
        }
        {Windows NT} {
            # Grab the device info set for present objects only
            set dfs [twapi::devinfoset -presentonly true]
            foreach element [twapi::devinfoset_elements $dfs] {
                catch {twapi::devinfoset_element_registry_property $dfs $element class} classname
                if {$classname eq "Ports"} {
                    # USB/serial ports are part of the "ports" class                    
                    catch {twapi::devinfoset_element_registry_property $dfs $element friendlyname} friendlyname
                    puts "Friendlyname is $friendlyname"
                    # Get the COMn number from the friendlyname.  Friendlynames look like
                    #
                    # USB Serial Device (COM5)
                    set vcp [lindex [split $friendlyname "()"] 1]
                    catch {twapi::devinfoset_element_registry_property $dfs $element mfg} mfg
                    puts "  Manufacturer is $mfg"                    
                    catch {twapi::devinfoset_element_registry_property $dfs $element hardwareid} hardwareid_list
                    # There may be more than one entry in the hardare ID list.  Just use the first one.
                    set hwid [lindex $hardwareid_list 0]
                    puts "  Hardwareid is $hwid"                                       
                    set vendor_id [get_windows_hwid_value $hwid "VID_"]
                    set product_id [get_windows_hwid_value $hwid "PID_"]
                    set composite_index [get_windows_hwid_value $hwid "MI_"]
                    puts "  Vendor ID: $vendor_id"
                    puts "  Product ID: $product_id"
                    puts "  COM port: $vcp"
                    puts "  Composite index: $composite_index"
                    dict set vcp_dict $vcp vendor_id $vendor_id
                    dict set vcp_dict $vcp product_id $product_id
                    dict set vcp_dict $vcp composite_index $composite_index
                }
            }
            twapi::devinfoset_close $dfs
        }
        default {
            # This should never happen
            puts "Failed to detect OS"
        }
    }

    return $vcp_dict
}

puts [detect_vcps]