Local hardware-control setup using microcontrollers as peripherals
Go to file
Mike Kazantsev 68123210c0
README.rp2040-usb-ppps: add link to related blog post
2023-11-30 02:46:56 +05:00
COPYING Add basic docs, old arduino fw, to be replaced with rp2040 controller 2023-11-15 02:56:45 +05:00
README.rst README.rp2040-usb-ppps: add link to related blog post 2023-11-30 02:46:56 +05:00
hwctl.py hwctl: log commands with -v/--verbose, minor misc tweaks 2023-11-16 22:51:31 +05:00
rp2040-usb-ppps.py README.rp2040-usb-ppps: add link to related blog post 2023-11-30 02:46:56 +05:00



Local hardware-control setup using microcontrollers as peripherals

Microcontroller firmware and linux userspace scripts/services to control things like LEDs, relays, buttons/switches and sensors connected to pins on those mcus in my local setup.

Cheap hobbyist microcontrollers like Arduinos and RP2040 boards are easy to use as USB peripherals for normal PCs, to provide smart programmable interface to a lot of simplier electrical hardware, electronics and common embedded buses like I²C.

E.g. for stuff like switching power of things on-and-off with relays by sending a command byte to /dev/ttyACM0, when it is needed/requested by whatever regular OS - for example on click in some GUI, desktop keyboard shortcut, or when some cronjob runs and needs external USB drive powered on.

Scripts here implement firmware and software side for some of my local setup, and are probably not useful as-is to anyone else, as they have specific pins connected to specific local hardware, so probably only useful for reference and/or as a random example snippets.

It's very much in a "local hardware/firmware projects dump" or "dotfiles" category.

Repository URLs:


RP2040 firmware used to control USB per-port-power-switching, but NOT via actual built-in ppps protocol that some USB Hub devices support (and can be done via sysfs on linux, or uhubctl tool), and instead via cheap MOSFET solid-state-relays, soldered to a cheap simple USB Hub with push-button power controls on ports.

Hubs with ppps have two deal-breaking downsides for me:

  • They're impossible to find or identify - e.g. any model on uhubctl list of "known working" is either too old, can't be sourced from here, or is ridiculously expensive, while USB3 Hubs with port power dpdt switches are dirt-common and cost like $10.

    Chips in many hubs support ppps, but sometimes it toggles data lines and not VBUS, sometimes it only toggles one "fast charging" port, most times it doesn't do anything at all (control tracks not connected to anything), all this changes between minor hw revisions, and afaict not mentioned anywhere by vendors.

  • PPPS in USB Hubs has default port power state as ON.

    So whenever you reboot the machine, dual-boot it into gaming-Windows or whatever, all the junk plugged into all ports powers-on all at once, which is dumb and bad - that's kinda the idea behind port power control to avoid this, and it takes its toll on devices (esp. spinning-rust ext-hdds).

    Also usually don't want non-main OS accessing stuff like Yubikeys at all (that lock themselves up after N access attempts), so power to those should always be default-disabled. In some cases, whole point of this per-port power control is to avoid seldom-used USB devices getting jerked around all the time, and ppps with default-on state is actually worse than simple always-on ports.

Controlling power via $1 SSRs soldered to buttons neatly fixes the issue -nothing gets randomly powered-on, and when it does, code on the rp2040 controller can be smart enough to know when to shut down devices if whatever using them stops sending it "this one is still in use" pings.

Implemented using mostly-stateless protocol sending single-byte commands over ttyACM (usb tty) back-and-forth, so that there can be no "short read" buffering issues.

When using mpremote with RP2040, mpremote run rp2040-usb-ppps.py won't connect its stdin to the script (at least current 2023 version of it), so right way to actually run it seem to be uploading as main.py and do mpremote reset or something to that effect.

For deploying script as long-term firmware, pre-compiling it via mpy-cross tool is probably a good idea:

% mpy-cross -march=armv6m -O2 rp2040-usb-ppps.py -o usb_ppps.mpy
% echo 'import usb_ppps; usb_ppps.run()' >loader.py
% mpremote cp usb_ppps.mpy :
% mpremote cp loader.py :main.py
% mpremote reset

(mpy-cross binary used there is trivial to build - see Arch PKGBUILD here)

Also wrote-up some extended thoughts on this subject in a "USB hub per-port power switching done right" blog post.


Linux userspace part of the control process - a daemon script to talk to connected microcontrollers and send them commands, received via whatever simple unixy IPC mechanisms.

Currently itself controlled via signals (e.g. pkill -USR1 -F hwctl.pid, see -p/--pid-file option) and any space/line-separated plaintext commands to a FIFO pipe (echo usb3=on >hwctl.fifo, -f/--control-fifo option) from terminal or scripts.

Uses serial_asyncio module from pyserial/pyserial-asyncio for ttyACMx communication.

Older version used to poll /proc/self/mountinfo fd and do some "don't forget to unmount" indication via LEDs connected to Arduino Uno board (running hwctl.ino), read/debounce physical buttons, as well as similar usb-control wdt logic as rp2040-usb-ppps script.