Close

udev rules

A project log for MKS TFT28 with Klipper

Using an MKS TFT28 touchscreen with Klipper and OctoPrint, without using alternative firmware on the touchscreen or sacrificing function.

setecastronomySetecAstronomy 01/23/2022 at 22:020 Comments

This whole complicated system I'm about to try involved three USB serial ports. The two USB serial TTL adapters, and the one in the printer. Sure we could probably set everything up using the various /dev/ttyUSB* ports, but what happens when the printer gets turned off and back on? What happens when the RasPi restarts? This is where udev comes in handy.

We can write some udev rules such that each port gets assigned an additional symlink of a name we chose, and we can use *those* names. So the printer is always /dev/ttySidewinder for instance.

Unfortunately, the USB serial-TTL adapters I've got don't have unique serial numbers (I know this from previous udev experiments). This makes it rather difficult to tell them apart with a udev rule. The only way I could come up with was to specify which port they were connected to. This isn't great if everything gets disconnected and reconnected in a different order, as then it wont work right. But short of ordering more parts, this is the solution I've got.

The first order of business is to figure out which port is which. To do this, I made sure that the printer was the only USB serial device connected, and then plugged it into the port I wanted to use for the PC serial-TTL adapter. Then it was something like this:

$ ls /dev/ttyUSB*
/dev/ttyUSB1

 So it looks like /dev/ttyUSB1 is our printer at the moment, we'll need that device name in our next command:

$ udevadm info --name=/dev/ttyUSB1 --attribute-walk
[lots and lots of output]

 I got several screens of output from this. The bit we're most interested in, was (in my case) the fourth device from the top. The first one that has a KERNELS== that looks something like this one, but without a colon in it:

  looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5':
    KERNELS=="1-1.5"

 That's going to be the bit I use to distinguish this port from another one. I copied that whole KERNELS== line and stuck it in a document with a note as to which port it was associated with.

Then I repeated the steps for the USB port I wanted to use for the Printer serial-TTL adapter.

Once I had a way to distinguish the two USB ports we intend to use for the serial-TTL adapters, I needed to find out their details so I could actually write a udev rule. I moved the 3D printer's USB cable, and plugged in the two USB serial-TTL adapters, taking care to get them into the correct ports. If you saw their photo in the previous log, I had labeled them to make this step easier. I probably should have disconnected the 3D printer, but I hadn't. Thankfully it retained the same /dev/ttyUSB1 that it was using earlier.

I reran the udevadm info command again, this time on one of my new USB serial ports: 

 $ udevadm info --name=/dev/ttyUSB2 --attribute-walk

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.0/ttyUSB2/tty/ttyUSB2':
    KERNEL=="ttyUSB2"
    SUBSYSTEM=="tty"
    DRIVER==""

  looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.0/ttyUSB2':
    KERNELS=="ttyUSB2"
    SUBSYSTEMS=="usb-serial"
    DRIVERS=="pl2303"
    ATTRS{port_number}=="0"

  looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1-1.5:1.0':
    KERNELS=="1-1.5:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="pl2303"
    ATTRS{bNumEndpoints}=="03"
    ATTRS{bInterfaceSubClass}=="00"
    ATTRS{authorized}=="1"
    ATTRS{bInterfaceClass}=="ff"
    ATTRS{bInterfaceProtocol}=="00"
    ATTRS{bInterfaceNumber}=="00"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{supports_autosuspend}=="1"

  looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5':
    KERNELS=="1-1.5"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{devnum}=="8"
    ATTRS{idProduct}=="2303"
    ATTRS{rx_lanes}=="1"
    ATTRS{devpath}=="1.5"
    ATTRS{urbnum}=="26"
    ATTRS{speed}=="12"
    ATTRS{devspec}=="(null)"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{bDeviceClass}=="00"
    ATTRS{removable}=="removable"
    ATTRS{bMaxPacketSize0}=="64"
    ATTRS{authorized}=="1"
    ATTRS{bcdDevice}=="0300"
    ATTRS{product}=="USB-Serial Controller"
    ATTRS{ltm_capable}=="no"
    ATTRS{maxchild}=="0"
    ATTRS{bMaxPower}=="100mA"
    ATTRS{tx_lanes}=="1"
    ATTRS{manufacturer}=="Prolific Technology Inc."
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{idVendor}=="067b"
    ATTRS{bmAttributes}=="a0"
    ATTRS{busnum}=="1"
    ATTRS{quirks}=="0x0"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{version}==" 2.00"
    ATTRS{configuration}==""

 I can already tell from the KERNELS==1-1.5 that it's the one plugged into the USB port I set aside for the PC connection. The important bits I'm looking for however are the ATTRS{idVendor}=="067b" and ATTRS{idProduct}=="2303". These combined with a few other bits will give us a rule that only applies to these specific devices, and the KERNELS== bits we gather beforehand will let us specify which one is which based on which port they're in.

Next thing I did was create the file /etc/udev/rules.d/99-custom-serial.rules:

SUBSYSTEM=="tty",SUBSYSTEMS=="usb",DRIVERS=="usb",ATTRS{idVendor}=="067b",ATTRS{idProduct}=="2303",KERNELS=="1-1.5",SYMLINK+="ttyTFT28_PC"
SUBSYSTEM=="tty",SUBSYSTEMS=="usb",DRIVERS=="usb",ATTRS{idVendor}=="067b",ATTRS{idProduct}=="2303",KERNELS=="1-1.4",SYMLINK+="ttyTFT28_Printer"
SUBSYSTEM=="tty",SUBSYSTEMS=="usb",DRIVERS=="usb",ATTRS{idVendor}=="1a86",ATTRS{idProduct}=="7523",SYMLINK+="ttySidewinder"

SUBSYSTEM=="tty" ensures we are working with a device that's handled by the bit that makes /dev/tty* devices. SUBSYSTEMS=="usb" and DRIVERS=="usb" limit us to USB parent devices. ATTRS{idVendor} matches the USB vendor ID, and ATTRS{idProduct} matches the product ID, those two limit us to a specific model of device. KERNELS matches to a specific port. SYMLINK+= assigns an additional symlink, with the provided name, pointing back to the original device.

In this case I made three devices:

ttyTFT28_PCUSB serial-TTL adapter that OctoPrint will use
ttyTFT28_PrinterUSB serial-TTL adapter that will be connected to /tmp/printer
ttySidewinderUSB connection to MKS Gen_L controller in Sidewinder X1 printer

Of course, they don't exist *yet*, I need to tell udev to reprocess all the rules:

$ sudo udevadm control --reload-rules && sudo udevadm trigger

Did it work?

$ ls /dev/ttyS* /dev/ttyT* -l
lrwxrwxrwx 1 root root 7 Jan 23 15:58 /dev/ttySidewinder -> ttyUSB1
lrwxrwxrwx 1 root root 7 Jan 23 15:58 /dev/ttyTFT28_PC -> ttyUSB2
lrwxrwxrwx 1 root root 7 Jan 23 15:58 /dev/ttyTFT28_Printer -> ttyUSB3

Woo! Next up, I need to connect /dev/ttyTFT28_Printer to /tmp/printer. A systemd service seems like the way to go, something that can run alongside the Klipper service.

I installed socat (the apt package name is socat :) ) After some experimenting I determined that A) Klipper doesn't use an systemd service, it used an old init.d script. And B) I couldn't for the life of me figure out how to get my new service to come back up when Klipper came back up. Which meant if Klipper was ever restarted from the OctoPrint GUI, there'd be a problem. The good news is that even though Klipper isn't using a systemd unit file, systemd still treats it as if it were. So I ended up making a drop-in file to start socat after Klipper starts: /etc/systemd/system/klipper.service.d/tft28.conf

[Unit]
ExecStartPost=/usr/bin/socat /dev/ttyTFT28_Printer,raw,echo=0,crnl /tmp/printer,raw,echo=0,crnl

And then reloaded systemd and restarted Klipper:

$ sudo systemctl daemon-reload
$ sudo systemctl restart klipper

I was then able to reconfigure OctoPrint to use /dev/ttyTFT28_PC and connect it, observe it happily polling the printer for the current temp, and receiving it back. So the serial communication was good between OctoPrint and Klipper, despite everything we added in between.

The next step will be opening the printer and connecting the TFT, which ought to be a much shorter log than this one :)

Discussions