going-flying.com gemini git repository
6ac91693be893decd31c099bf12d8eeda5048300 - Matthew Ernisse - 1596030457
new post!
diff --git a/files/vfdserver.py b/files/vfdserver.py new file mode 100755 index 0000000..b17bbfb --- /dev/null +++ b/files/vfdserver.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +''' vfdserver (c) 2020 Matthew J. Ernisse <matt@going-flying.com> +All Rights Reserved. + +Redistribution and use in source and binary forms, +with or without modification, are permitted provided +that the following conditions are met: + + * Redistributions of source code must retain the + above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce + the above copyright notice, this list of conditions + and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''' +import datetime +import http +import http.server +import serial +import sys +import urllib.parse + + +vfdSerial = None + + +class SerialController(object): + def __init__(self, serialPort): + self.serial = serial.Serial( + port=serialPort, + baudrate=115200, + timeout=1.0, + write_timeout=1.0 + ) + + def write(self, s): + if type(s) is not str: + raise ValueError('message MUST be str') + + now = datetime.datetime.now() + dateStr = now.strftime('%Y-%m-%d @%H:%M:%S') + + cmd = f'MSG 1 {dateStr}\r\n'.encode('utf-8') + self.serial.write(cmd) + + cmd = f'MSG 2 {s}\r\n'.encode('utf-8') + self.serial.write(cmd) + + +class VFDHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + req = urllib.parse.urlparse(self.path) + + if req.path != '/update': + response = 'Not Found'.encode('utf-8') + self.send_response(http.HTTPStatus.NOT_FOUND) + self.send_header( + 'Content-Type', + 'text/plain' + ) + self.end_headers() + self.wfile.write(response) + self.log_request(404, 0) + return + + qs = urllib.parse.parse_qs(req.query) + print(qs) + if not 'msg' in qs.keys(): + response = 'Invalid Request Data'.encode('utf-8') + self.send_response(http.HTTPStatus.BAD_REQUEST) + self.send_header( + 'Content-Type', + 'text/plain' + ) + self.end_headers() + self.wfile.write(response) + self.log_request(400, 0) + return + + try: + vfdSerial.write(qs['msg'][0]) + except Exception as e: + response = str(e).encode('utf-8') + self.send_response( + http.HTTPStatus.INTERNAL_SERVER_ERROR + ) + self.send_header( + 'Content-Type', + 'text/plain' + ) + self.end_headers() + self.wfile.write(response) + self.log_request(500, 0) + return + + self.send_response(http.HTTPStatus.NO_CONTENT) + self.end_headers() + self.log_request(204, 0) + + +if __name__ == '__main__': + vfdSerial = SerialController(sys.argv[1]) + + server = http.server.ThreadingHTTPServer( + ('0.0.0.0', 80), + VFDHandler + ) + server.serve_forever() diff --git a/users/mernisse/articles/02.gmi b/users/mernisse/articles/02.gmi new file mode 100644 index 0000000..2ada31f --- /dev/null +++ b/users/mernisse/articles/02.gmi @@ -0,0 +1,138 @@ +--- +Title: Automating daemon start on USB plug with systemd +Date: 07/29/2020 09:30 + +Lets start out with this, just to get it out of the way. I hate systemd. +I think what it does is great but I think the user-facing bits of it are +awful. I also hate how it's been railroaded into way too much of the +system and yet for some reason the Linux kernel folks continue to refuse +to implement a lightweight message bus which means even a Raspberry Pi +gets to run dbus. + +Ok, that is out of the way, as I mentioned in my last note I hooked a VFD +up to a CGI on my Gemini server. The end of that chain is a small Python +HTTP server that translates a command into a serial command to send to the +VFD. The VFD is connected to an ATmega32U4 which provides serial emulation +over USB-CDC. Initially for testing I was just running the vfdserver +process inside tmux but that's not at all a long-term solution so I set about +trying to figure out how to get it to automatically run. + +=> 01.gmi + +## Round 1 + +The easiest thing to do is just to create a systemd unit to run the server +whenever. As long as the VFD is plugged in at boot this is fine, however +if the VFD is not present then the process will terminate after throwing an +exception over and over. That being said, it was what I tried first. + +### /etc/systemd/system/vfdserver.service +``` +[Unit] +Description=Serial VFD server +After=network.target + +[Service] +Type=simple +Restart=always +User=root +ExecStart=/usr/sbin/vfdserver.py /dev/ttyACM0 + +[Install] +WantedBy=multi-user.target +``` + +Two things bothered me about this though. I don't particularly like the idea +of the server restarting over and over if I have to unplug the VFD to flash +new firmware to it. Also it seems that if I re-plug it with the system running +it may show up as /dev/ttyACM1 instead of /dev/ttyACM0 so that is not ideal. + +## Round 2 + +Obviously now I wanted to see if I could create a stable device name for the +USB serial port. Having it flop around after unplug/plug is not ideal. +Thankfully I've done this before with network interfaces so it was pretty +easy to just add a udev rule to give this particular USB device a specific +name. + +### /etc/udev/rules.d/99-vfd.rules +``` +SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", ATTRS{idProduct}=="000d", SYMLINK+="ttyUSB-VFD", GROUP="dialout", MODE="0666" +``` + +I set the USB vendor ID and product ID in the firmware of the device so it was +trivial to make sure it was unique to this particular unit. This makes it +always create a /dev/ttyUSB-VFD which I can now use in my systemd unit in place +of /dev/ttyACM0. Awesome. + +## Round 3, Fight! + +But that restarting thing. It bugged me that the system would be sitting there +continually trying to run a server that had no shot in hell of starting. I +also didn't want to make the script sit around and wait for the device to show +up and then handle disconnect and reconnect itself. I mean it's like 90 lines +of Python and based on the standard library's http.server.ThreadingHTTPServer +so not exactly complex and I didn't really want to make it more complex. A +little bit of digging shows that I can trigger systemd on device events. This +makes sense to me given it at some point absorbed udev. Step one seems to +be telling systemd about the device, that was a simple change in the rules +file to add the systemd tag. + +### /etc/udev/rules.d/99-vfd.rules +``` +SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", ATTRS{idProduct}=="000d", SYMLINK+="ttyUSB-VFD", GROUP="dialout", MODE="0666", TAG+="systemd" +``` + +Now the device will show up as a systemd unit. + +### systemctl status /dev/ttyUSB-VFD +``` +● dev-ttyUSB\x2dVFD.device - 2x24_Line_VFD_Display + Follow: unit currently follows state of sys-devices-platform-soc-3f980000.usb-usb1-1\x2d1-1\x2d1.3-1\x2d1.3:1.0-tty-ttyACM0.device + Loaded: loaded + Active: active (plugged) since Wed 2020-07-29 09:21:33 EDT; 18min ago + Device: /sys/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.3/1-1.3:1.0/tty/ttyACM0 +``` + +Once systemd knows about it we can list it as a reverse dependancy on our +service and somewhat shockingly systemd will do the right thing. + +### /etc/systemd/system/vfdserver.service +``` +[Unit] +Description=Serial VFD server +After=network.target + +[Service] +Type=simple +Restart=always +User=root +ExecStart=/usr/sbin/vfdserver.py /dev/ttyUSB-VFD + +[Install] +WantedBy=dev-ttyUSB\x2dVFD.device +``` + +I plugged the display in and systemd promptly started up the server process. + +``` +● vfdserver.service - Serial VFD server + Loaded: loaded (/etc/systemd/system/vfdserver.service; enabled; vendor preset: enabled) + Active: active (running) since Wed 2020-07-29 09:21:33 EDT; 20min ago + Main PID: 1332 (python3) + Tasks: 1 (limit: 2068) + CGroup: /system.slice/vfdserver.service + └─1332 python3 /usr/sbin/vfdserver.py /dev/ttyUSB-VFD +``` + +## Conclusion + +Ok, so I was a little surprised it was that easy. I sort of expected there +to be some kind of hackyness involved but at the same rate I guess it shouldn't +be that shocking, running services on device plug events is kind of a big +deal in the desktop world so it really *should* be a solved problem. Anyway +I hope this look into how I built my silly little Gemini connected VFD display +was interesting, or at least if not interesting helpful if you ever find +yourself screwing around with systemd and udev. 🔨 + +=> /files/vfdserver.py vfdserver.py source