Other How to lock serial ports

I find myself having to use multiple serial ports for my home data acquisition software. One of the problems with that is that the acquisition program needs to have exclusive access to the serial port: If someone uses cu or minicom or even just cat /dev/ttyXX | hexdump -C while my software is running, bad things happen. (I'll add a footnote below to explain why this problem is worse than one thinks, and can't just be solved with discipline).

So I need a way to lock a serial port: Once a program starts using it, it needs to make sure no other program will touch it. This is an absolutely standard and boring problem, and there are three known solutions to it:
  1. Use a lock file: When you open /dev/ttyXX, you also create /var/lock/LCK..ttyXX, and there is a well-known technique of what should be written into that lock file. The lock file gets deleted when the program stops using the serial port. All programs need to check for that lock file before touching a port.
  2. Use standard Unix advisory file locking: Call flock()(2) and friends before and after using the device file. Note that this is advisory only, and again requires cooperation from all programs that use serial ports.
  3. When opening the device, change its ownership and permission so no other process can use it, and change it back when done. This works badly, because it doesn't help against root. And again, it only works if all programs cooperate (and this technique has other problems too).
Here is my question: In FreeBSD, which of the three techniques is traditionally used by most programs that touch serial ports? Because any of these techniques will only work for me if most other programs cooperate in using them. Anyone know whether there is a standard or tradition? I could read reams of source code, but I'm feeling lazy.

Footnote 1: Clearly, you can't guard against cat /dev/ttyXX >... or cat foo.txt > /dev/ttyXX, since the cat program doesn't use any locking, but that should be a rare abuse that we can ignore.

Footnote 2: There is another problem which needs to be solved, which makes solving this locking problem even more important: Since I have multiple devices connected via serial port (mostly using USB-to-serial adapters), I have no idea a priori which /dev/ttyXX file corresponds to which piece of hardware (water sensor, pressure controller, lighting interface, pump controller ModBus interface, ...). All these things use different baud rates, and different protocols. The way to identify the one you are looking for is: Promiscuously loop over all serial devices, set them to the desired baud rate, and send a "who are you" or "are you there" command using the desired protocol. If you get a syntactically correct response with the right ID number, you win. If you get nothing, or complete gibberish, you check the next one. The problem with this is: Whenever a program needs to find its hardware, it will send "garbage" to many devices, which those devices won't know how to interpret, or which will screw up the protocol for the real user of that other device. So once a program finds the correct serial device, it really should lock it, to prevent other programs from wasting their time on checking it, and perhaps breaking its own communication.
 
The flock(2) approach looks good as a partial fix. There's a flag on open(2) to give you a flock-style advisory lock up-front.

If you have control over the protocol, use a protocol that can detect and recover from spurious data. Examples: the VISA II protocol, CI/V, anything designed to run on RS-485. Something with a check-sum and bookends (when a message begins/ends) will do.

Random thought: isolate the programs in separate jails or bhyve VMs - and limit which ports are visible in each jail / VM.
 
But do minicom/cu/... use the flock approach? I'll check on that when I'm more awake.

I do not have control over the protocol. It is forced on me by vendors. In some cases it's well designed, and easy to perform error handling. In other case ... it was not well designed, and we'll have to leave it at that (if you can't say anything nice, don't say anything at all).

The idea of isolating ports to jails solves the problem of a serial port not being disturbed by other programs. It doesn't solve the problem of how to determine which port has what hardware attached. And the idea of having to move ports from jail to jail dynamically makes my skin itch.

By the way, what I didn't mention: This is not a problem with the standard (motherboard) serial ports. Their numbering doesn't change (at least not often, only when you replace the motherboard or physically install add-on cards). The problem is USB-connected serial ports, which can come and go, be renumbered when they show up again, be discovered in different order, and so on. To make matters more "interesting", some vendors of USB-connected hardware (Omega and Newport process controllers for example) don't implement their serial ports as USB serial ports, but as USB modems.
 
Clearly you understand the problem. You've pointed one some techniques to reduce the risk. Here is one more.

For USB emulated ports, you can distinguish different make / model by the USB VID/PID. For identical devices, there is usually a unique serial number.

You can quickly fingerprint all of your USB devices with usbconfig:
Code:
# sudo usbconfig | grep 'product' 

ugen2.2: <vendor 0x8087 product 0x8000> at usbus2, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen1.2: <vendor 0x8087 product 0x8008> at usbus1, cfg=0 md=HOST spd=HIGH (480Mbps) pwr=SAVE (0mA)
ugen0.3: <vendor 0x138a product 0x0017> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)
ugen0.4: <vendor 0x8087 product 0x07dc> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)
If you have two identical devices, pull the serial number.
Code:
# sudo usbconfig -u 0 -a 2 dump_device_desc

ugen0.2: <Logitech USB Receiver> at usbus0, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (98mA)

bLength = 0x0012
bDescriptorType = 0x0001
bcdUSB = 0x0200
bDeviceClass = 0x0000 <Probed by interface class>
bDeviceSubClass = 0x0000
bDeviceProtocol = 0x0000
bMaxPacketSize0 = 0x0008
idVendor = 0x046d
idProduct = 0xc52f
bcdDevice = 0x3000
iManufacturer = 0x0001 <Logitech>
iProduct = 0x0002 <USB Receiver>
bNumConfigurations = 0x0001
The "serial number" is actually a string. The example above doesn't have one so this isn't 100% but can greatly reduce this headache.

No doubt you plan to lock down your environment so that monitoring / controlling software don't run as root.
 
This might be a silver bullet but will require lots of testing.

Modify the kernel to always apply flock(2) eg. O_EXLOCK to any attempt to open(2) a serial port. Bonus points for enabling this as a sysctl(8) tunable.

Coming soon: Ralph's "Industrial" edition of FreeBSD!
 
I really like the USB vendor/product/serial number technique. Should have thought of that, great suggestion. Thank you. That's something I can code up in a long evening in Python, without straining any important body parts, and probably even while having a few glasses of wine. Afterwards, I can jump in the hot tub (after all, we're in California here).

Mandatory locking sounds like real work: it would not be compatible with the glasses of wine and hot tub. For a hobby, that's a bit heavyweight. But it's a good suggestion; very tempting. By the way, another way to implement it (instead of a sysctl) would be to use an ACL or file permission bit, which when set on a file forces mandatory locking on open. For some reason, I get the feeling that I've heard that idea before, probably some hallway conversation at a Usenix conference or something like that.
 
Back
Top