Tuesday, October 4, 2016

udevadm, systemd and a barcode scanner

I've been fooling around with a Symbol LS2208 barcode scanner attached to a CentOS 7 machine as part of a network automation project. I learned a bit about the scanner, udev and systemd along the way.

The LS2208
I chose the LS2208 because there were lots of them on eBay and because documentation was available. So far I'm happy with the LS2208, but wish it didn't require a physical PC to be nearby. A USB Anywhere box may be in my future (nope, Windows only). If I'd been able to find a WiFi scanner that would POST scans directly to a REST API over TLS, I'd have gone with that instead, but it seems that this guy and I are out of luck in that regard. I've got zero interest in fooling around with WinCE or similar mobile devices with built-in scanners.
The LS2208 gets configured by scanning barcodes. Special codes found in the manual. So far, the ones I've found most interesting are:
  • Set Factory Defaults
  • Simple COM Port Emulation
  • Low Volume
  • Beep on <BEL> (still need to fool with this - seems like it could provide useful feedback to the operator)
  • Do Not Beep After Good Decode
By default the scanner appears with USB vendor/product codes 0x05e0/0x1200 which makes it emulate a USB keyboard. Fun to play with, but not how I want it to work for my project.

Scanning the Simple COM Port Emulation barcode found in the manual changes its USB personality to 0x05e0/0x0600, so that it shows up as an hidraw Linux device. I can't find any indication that it's a serial port, though the symbolserial device driver loads in CentOS 7.

With each scan, the device at /dev/hidrawX produces 64 bytes consisting of:
  • A single byte indicating the length of the scanned data. UPC codes, for example, are 12 bytes long so they always produce 0x0C as the first byte.
  • The scanned data
  • Padding with <NUL> characters up to 64 bytes in total
We can take a quick look at the scan data with: od -t x1 < /dev/hidraw0.

A tiny bit of python
This little program reads from the scanner and spits out the result:

 #!/usr/bin/env python  
 import argparse  
 import sys  
 defaultDevice = '/dev/scanner'  
 parser = argparse.ArgumentParser()  
 parser.add_argument('-d', '--dev', dest='d', default=defaultDevice,  
           help='Scanner device; defaults to '+defaultDevice)  
 parser.add_argument('-o', '--out', dest='o', help='Output file')  
 args = parser.parse_args()  
 inDev = open(args.d, 'rb')  
 if args.o:  
  outFile = open(args.o, 'a')  
  outFile = sys.stdout  
 while True:  
  inBytes = inDev.read(64)  
  length = ord(inBytes[:1])  
  value = inBytes[1:length+1]  

Not wanting to count on the scanner always landing on /dev/hidraw0, the next task was to write a udev rule to make it easy to find. I wound up with the following in /etc/udev/rules.d/10-scanner.rules:

 KERNEL=="hidraw[0-9]*", ATTRS{idVendor}=="05e0", ATTRS{idProduct}=="0600", GROUP="barcode", ACTION=="add", SYMLINK="scanner", TAG+="systemd", ENV{SYSTEMD_WANTS}="BarcodeScanner@%N.service"  

Essentially, I'm matching hidraw devices with the appropriate vendor and product codes, setting the resulting device file to be owned by group barcode (a group I created for use by the scanner project) and creating a symlink: /dev/scanner -> /dev/hidraw0

When the device appears, udev will ask systemd will kick off the BarcodeScanner unit with an argument (%N in the udev rule) indicating the path to the scanner device.

Finally, we need the systemd unit file to go with that udev rule. Here's my /etc/systemd/system/BarcodeScanner@.service unit file:

 Description=Simple Symbol Scanner  
 ExecStart=/tmp/scan.py -d %I -o /tmp/scanned.txt  

The StopWhenUnneeded directive causes systemd to kill (SIGTERM, then SIGKILL if needed) the scan.py loop when nothing "wants" it. The %I evaluates to the device file passed in by udev.

Type=simple tells systemd that it shouldn't expect this unit to properly daemonize itself.

Poke systemd so that it notices the new unit file:

 sudo systemctl daemon-reload  

Now, when the scanner's USB cable is plugged in, the python code starts up and gets pointed (-d argument) at the scanner to start logging bar codes to /tmp/scanned.txt. When the scanner is unplugged, the BarcodeScanner unit is no longer "wanted" by anything, so systemd kills it off.