0

I'm using usb-1.3.0.4 / System.USB. Having scanned and selected a relevant serial USB device, how do I find the corresponding serial port (e.g. COM3 on Windows)?

Here is an example of what I'm after:

module Main (main) where

import System.USB
import System.Hardware.Serialport
import qualified Data.Vector as V
import Data.Maybe

main :: IO ()
main = do
    devicePort 0x2341 0x0043 >>= either putStrLn usePort
    where usePort p = do
              s <- openSerial p defaultSerialSettings
              putStrLn $ "opened serial port " ++ p
              closeSerial s

-- | Find port for attached USB serial device
devicePort :: VendorId -> ProductId -> IO (Either String FilePath)
devicePort vid pid = do
    ctx <- newCtx
    findDevice ctx vid pid >>= \md -> case md of
        Just dev -> fmap (maybe (Left "not a serial device") Right) $ serialPort dev
        Nothing -> return $ Left "device not found"

-- | Scan for first device with given vendor and product identifiers
findDevice :: Ctx -> VendorId -> ProductId -> IO (Maybe Device)
findDevice ctx vid pid = fmap (listToMaybe . V.toList) $ V.filterM p =<< getDevices ctx
    where p x = do
              d <- getDeviceDesc x
              return $ deviceVendorId d == vid && deviceProductId d == pid

serialPort :: Device -> IO (Maybe FilePath)
serialPort dev = undefined

What is a possible implementation of the last function?

marangisto
  • 260
  • 2
  • 8
  • On Windows I'd expect to see things like COM3 or COM4 while on Linux /dev/ttyusb0 or similar. This will ultimately feed into the openSerialPort function from System.Hardware.Serial. – marangisto Jul 24 '16 at 14:49

3 Answers3

2

You need to search for your device in the registry. Under the key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum there are multiple keys (in most cases its USB but sometimes the drivers install it in a different subkey) which themselves contain keys in the format VID_xxxx&PID_xxxx. You have to find this key first. It will be most likely something like HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\VID_xxxx&PID_xxxx.

This key contains itself new subkeys. And these subkeys contain a key called Device Parameters which contains the needed key value pair PortName with the port something like COM3.

Sadly I have no idea how this is done in Haskell.

dryman
  • 660
  • 6
  • 16
  • Thanks. Yes, I'm converging on using udev on Linux and some registry search as you suggest on Windows. I'll try to abstract these into a common API that works cross-platform. – marangisto Jul 25 '16 at 12:16
1

Following dryman's suggestion and some digging around I abandoned usblib altogether and implemented directly via the registry on Windows (I'll leave Linux for another day):

{-# LANGUAGE RecordWildCards #-}
module USBSerial (USBSerial(..), usbSerials) where

import System.Win32.Registry (hKEY_LOCAL_MACHINE, regOpenKey, regCloseKey, regQueryValue, regQueryValueEx)
import System.Win32.Types (DWORD, HKEY)
import Control.Exception (handle, bracket, SomeException(..))
import Foreign (toBool, Storable(peek, sizeOf), castPtr, alloca)
import Data.List.Split (splitOn)
import Data.List (stripPrefix)
import Numeric (readHex, showHex)
import Data.Maybe (catMaybes)
import Control.Monad (forM)

data USBSerial = USBSerial
    { key           :: String
    , vendorId      :: Int
    , productId     :: Int
    , portName      :: String
    , friendlyName  :: String
    }

instance Show USBSerial where
    show USBSerial{..} = unwords [ portName, toHex vendorId, toHex productId, friendlyName ]
        where toHex x = let s = showHex x "" in replicate (4 - length s) '0' ++ s

usbSerials :: Maybe Int -> Maybe Int -> IO [USBSerial]
usbSerials mVendorId mProductId = withHKey hKEY_LOCAL_MACHINE path $ \hkey -> do
    n <- regQueryValueDWORD hkey "Count"
    fmap catMaybes $ forM [0..n-1] $ \i -> do
        key <- regQueryValue hkey . Just . show $ i
        case keyToVidPid key of
            Just (vendorId, productId)
                | maybe True (==vendorId) mVendorId && maybe True (==productId) mProductId -> do
                    portName <- getPortName key
                    friendlyName <- getFriendlyName key
                    return $ Just USBSerial{..}
            _ -> return Nothing
    where path = "SYSTEM\\CurrentControlSet\\Services\\usbser\\Enum"

getPortName :: String -> IO String
getPortName serial = withHKey hKEY_LOCAL_MACHINE path $ flip regQueryValue (Just "PortName")
    where path = "SYSTEM\\CurrentControlSet\\Enum\\" ++ serial ++ "\\Device Parameters"

getFriendlyName :: String -> IO String
getFriendlyName serial = withHKey hKEY_LOCAL_MACHINE path $ flip regQueryValue (Just "FriendlyName")
    where path = "SYSTEM\\CurrentControlSet\\Enum\\" ++ serial

keyToVidPid :: String -> Maybe (Int, Int)
keyToVidPid name
    | (_:s:_) <- splitOn "\\" name
    , (v:p:_) <- splitOn "&" s
    , Just v <- fromHex =<< stripPrefix "VID_" v
    , Just p <- fromHex =<< stripPrefix "PID_" p = Just (v, p)
    | otherwise = Nothing
    where fromHex s = case readHex s of
            [(x, "")] -> Just x
            _         -> Nothing

withHKey :: HKEY -> String -> (HKEY -> IO a) -> IO a
withHKey hive path = handle (\(SomeException e) -> error $ show e) . bracket (regOpenKey hive path) regCloseKey

-- | Read DWORD value from registry.
-- From http://compgroups.net/comp.lang.haskell/working-with-the-registry-windows-xp/2579164
regQueryValueDWORD :: HKEY -> String -> IO DWORD
regQueryValueDWORD hkey name = alloca $ \ptr -> do
    regQueryValueEx hkey name (castPtr ptr) (sizeOf (undefined :: DWORD))
    peek ptr

For example:

main :: IO ()
main = usbSerials (Just 0x2341) Nothing >>= mapM_ print

produces output like:

COM7 2341 8036 Arduino Leonardo (COM7)
COM3 2341 0043 Arduino Uno (COM3)
marangisto
  • 260
  • 2
  • 8
0

If you really cannot solve it in another way, please disregard this answer.


You should consider other options. Instead of scanning for a device that matches specific vendor or product IDs, you could instead simply accept the serial interface's name as an argument to your program or as a configurable option. On Windows, this would be in the form "COMx", while on Unix it would simply be a path.

Additionally, serial interfaces may not always be USB devices, preventing you from enumerating PCI or integrated serial ports by scanning USB devices. Plus, hard-coding values into source code makes for a painful update when the device is later replaced with something else.

Static port naming

If you are on Windows, assigning a custom interface number (e.g. COM7) should make it stick if you unplug the interface or reboot the computer. On Linux, it is a bit more contrived: you can add a Udev rule that matches the product and vendor ID so that it will create a node with a custom name, for instance /dev/arduinoN. I strongly suggest you follow this approach, as giving a manual path or modifying a Udev file is (arguably) easier than recompiling your application. I know a DMX product that does this: since it uses an off-the-shelf FTDI interface, it ships with a rule that matches this vendor and product ID and renames the node /dev/dmxN. While the rule clashes with other FTDI interfaces, in your case it will not since Arduino have their own product and vendor ID assignment.

sleblanc
  • 3,821
  • 1
  • 34
  • 42
  • 1
    Thanks, for some applications and uses this manual approach works fine. But some devices create serial interfaces on new ports dynamically (e.g. the Arduino Leonardo bootloader comes in on a different port after a reset) and if you frequently switch multiple devices things get tedious fairly quickly. – marangisto Jul 25 '16 at 12:14
  • @marangisto, I added a section on static naming. – sleblanc Jul 25 '16 at 17:08