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')
inDev.flush()
if args.o:
outFile = open(args.o, 'a')
else:
outFile = sys.stdout
while True:
inBytes = inDev.read(64)
length = ord(inBytes[:1])
value = inBytes[1:length+1]
outFile.write(str(length)+"\t"+value+"\n")
outFile.flush()
udev
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:
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.
systemd
Finally, we need the systemd unit file to go with that udev rule. Here's my /etc/systemd/system/BarcodeScanner@.service unit file:
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:
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.
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.
systemd
Finally, we need the systemd unit file to go with that udev rule. Here's my /etc/systemd/system/BarcodeScanner@.service unit file:
[Unit]
Description=Simple Symbol Scanner
StopWhenUnneeded=yes
[Service]
Type=simple
ExecStart=/tmp/scan.py -d %I -o /tmp/scanned.txt
[Install]
WantedBy=multi-user.target
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.