gemini.git

going-flying.com gemini git repository

summary

tree

log

refs

6ac91693be893decd31c099bf12d8eeda5048300 - Matthew Ernisse - 1596030457

new post!

view tree

view raw

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