3D Drucker automatisch mit Octoprint in KVM VM verbinden.

Der Gedanke, einen echten PC als zentralen Rechner mit vielen Octoprint-Instanzen zu nutzen, ist naheliegend und preiswert. Dabei will man jedoch nicht zwangsweise auf dem Host selbst installieren, sondern in eine virtuelle Maschine. Das hat zahlreiche Vorteile, wie z.B. Snapshots und vereinfachte Hostmigration, bringt aber auch Komplexität mit sich.

uml diagram

Die virtuelle Maschine kann die Drucker nun nicht mehr direkt sehen, weil /dev auf dem Host nicht gleich /dev in der virtuellen Maschine ist. Daher müssen die Drucker dynamisch und möglichst automatisch an die die virtuelle Maschine durch gereicht werden. Unter Linux kein Problem, dank udev.

Udev-Regel im VM Host erstellen

Die Regeln für udev sind leicht erstellt. Vorher braucht man aber den Identifikator des Gerätes. Ich nehme meist

watch -n .1 lsusb

und verbinde dann das Gerät. So sehe ich schnell, welches Gerät dazu kommt und welchen Identifikator es hat. Mein Drucker sieht dann so aus. Die ID ist die entscheidende Information.

Bus 003 Device 007: ID 1a86:7523 QinHeng Electronics CH340 serial converter

Nun die Konfigurationsdatei in /etc/udev/rules.d/20_3dprinter.rules erstellen. Meine sieht so aus.

ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{DEVTYPE}=="usb_device", RUN+="/opt/scripts/attach_wanhaoi3plus.sh"

Damit wird beim Anstecken des Gerätes ein Skript ausgeführt. Udev kann leider keine Argumente an die ausgeführten Skripte weitergeben. Daher braucht jeder Drucker ein eigenes Skript.

Dann das Skript in /opt/scripts/attach_wanhaoi3plus.sh erstellen.

set -x
DEVICE="device octoprint --file /opt/scripts/wanhao_usb.xml --persistent --live"

virsh attach-"$DEVICE"
if [ $? != 0 ]; then
  virsh detach-"$DEVICE"
  virsh attach-"$DEVICE"
fi
sleep 2

Da udev in der VM nicht wissen kann, ob das Gerät noch mit dem Host verbunden ist, muss dieser Fehlerfall behandelt werden. Wenn das Gerät noch an der virtuellen Maschine hängt, aber neu angesteckt wird und die udev-Regel auslöst, ist es sowieso dysfunktional. Die virtuelle Maschine hat halt nicht mitbekommen, dass das Gerät nicht mehr da ist. Das kann durch kaputte Kabel etc. schon mal passieren.

Wie man im Skript bereits sieht, ist die Ressourcendefinition für KVM eine .xml-Datei. Die ist dankenswerterweise sehr kurz. Es sollte auch auf Anhieb klar sein, dass hier die ID von weiter oben erneut angepasst werden muss.

<hostdev mode='subsystem' type='usb' managed='yes'>
   <source>
     <vendor id='0x1a86'/>
     <product id='0x7523'/>
   </source>
</hostdev>

Anschließend mit

udevadm control --reload-rules

udev neu laden, damit die neu erstellte Regel funktioniert. Wenn alles geklappt hat, ist das Gerät unter der exakt gleichen ID in der virtuellen Maschine zu finden sein. Die Ausgabe von lsusb | grep '<ID>' sollte nicht leer sein.

Drucker automatisch mit Octoprint verbinden

In der virtuellen Maschine übernimmt zuerst wieder udev den entsprechenden Trigger. Die Datei /etc/udev/rules.d/20_3dprinter.rules ist der oben genannten sehr ähnlich. Es wird aber zusätzlich ein statischer Alias namens wip hinzugefügt, damit das Gerät im System immer einen eindeutigen Namen bekommt. Statt eines Skriptes wird auch ein Systemd-oneshot-Service gestartet.

ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="wip", TAG+="systemd", ENV{SYSTEMD_WANTS}="octoprint_connect@wip.service"

Das Editieren dieser Datei wieder mit udevadm control --reload-rules abschließen.

Die Servicedefinition für den oneshot-Service liegt in /etc/systemd/system/octoprint_connect@.service und hat folgenden Inhalt. Die Platzhalter sind der eigentliche Gimmick, denn so lassen sich für jeden definierten Alias eine eigene Octoprint-Instanz ansprechen. Somit kann man verschiedene Drucker auf dem gleichen Host betreiben und braucht nicht für jeden Drucker ein eigenes OS.

[Unit]
Description=Connect printer to OctoPrint automatically
BindsTo=dev-%i.device
After=dev-%i.device

[Service]
Type=oneshot
User=moe
RemainAfterExit=yes
ExecStart=/home/octoprint/%I/connect_octoprint.py /dev/%I

Falls Octoprint noch kein Service ist, dann /etc/systemd/system/multi-user.target.wants/octoprint_wip.service erstellen.

[Unit]
Description=The snappy web interface for your 3D printer
After=network-online.target
Wants=network-online.target

[Service]
Environment="LC_ALL=C.UTF-8"
Environment="LANG=C.UTF-8"
Type=exec
User=octoprint
ExecStart=/home/octoprint/wip/OctoPrint/bin/octoprint serve

[Install]
WantedBy=multi-user.target

Änderungen an Systemd mit systemctl daemon-reload abschließen und dann die Services aktivieren.

systemctl enable octoprint_wip.service --now
systemctl enable octoprint_connect@wip.service

Für den letzten Schritt brauch man noch einen API-Key aus der Instanz. Den bekommt man unter Einstellungen->API im Octoprint Webinterface. Dieser Key wird verwendet, um sich in Octoprint zu authentifizieren und den Drucker anzumelden. Das funktioniert über eine REST-API. Das Skript aus o.g. Unitfile liegt in /home/octoprint/wip/connect_octoprint.py.

#!/home/octoprint/wip/OctoPrint/bin/python

OCTOPRINT_URL = 'http://localhost:5000/api/connection'
API_KEY = '<APIKEY>'
BAUDRATE = 115200


import requests
import sys

port = sys.argv[1]
headers = {'X-Api-Key': API_KEY}
json = {
    "command": "connect",
    "port": port,
    "baudrate": BAUDRATE,
}

r = requests.post(
        OCTOPRINT_URL,
        json=json,
        headers=headers
    )

if (r.status_code == 204):
    sys.exit(0)
else:
    print(r)
    sys.exit(1)

Das Ganze Konstrukt ist ziemlich Robust und hat bisher nach über einem Jahr Betrieb immer funktioniert.