My Logitech K380 defaults to sending media keys instead of F1-F12. This is maddening when your computing environment makes heavy use of F-keys.

There’s a neat little Python script, k380fnlk by Mike Ribbons, that toggles it by writing a raw HID report to the keyboard. It relies on hidapi, and it’s exactly what I needed. Except I run Fedora Kinoite, an atomic/immutable desktop, and I do all my day-to-day work inside a toolbox container. pip install -r requirements.txt failed compiling hidapi’s C extension, so I did the obvious thing: sudo dnf install -y whatever header the compiler complained about next, one error at a time — libudev.h, pulled in by systemd-devel, among others. Fine, that’s all container packages, not host ones. Once it finally built, the script ran and calmly reported “K380 keyboard not found,” which was odd, since the OS clearly saw the keyboard just fine.

Turned out hidapi’s Python wheel actually ships two separate native modules on Linux: hid, built against libusb, and hidraw, built against the kernel’s hidraw interface. Plain import hid — what the script used — only enumerates real USB devices. A Bluetooth-paired K380 isn’t one; it shows up to the kernel as a virtual uhid device. Swapping to import hidraw found it immediately.

Then came the permissions story. /dev/hidrawN nodes are root-only by default, and the udev rule commonly suggested for this (matching ATTRS{idVendor}/ATTRS{idProduct}) only works for USB device trees — a Bluetooth HID device’s sysfs ancestry doesn’t have those attributes at all, so the rule silently never fires. The fix is matching on the HID bus id instead: KERNELS=="0005:046D:B342.*", which works whether the keyboard is on Bluetooth or a USB receiver.

And then, because it wouldn’t be a Bluetooth story without one more twist: I switched the keyboard to one of its other pairing channels and back, and BlueZ got stuck with Connected: true but ServicesResolved: false. Typing still worked — interrupt reports don’t seem to care — but the feature-report write used to toggle Fn-lock would silently “succeed” while doing nothing at all, which is a genuinely unpleasant thing to debug.

All of this needs to run unattended, fired by udev the instant the keyboard reconnects. A Python interpreter plus a compiled C extension with its own header dependency is a lot of moving parts to keep alive on a base OS image I’d rather not layer packages onto.

So I rewrote it: k380-fnlk is a ~50-line C program that does nothing but open() and write() the two magic 7-byte HID feature reports straight to /dev/hidrawN. No libusb, no hidapi, nothing beyond glibc, which is on every Linux box regardless of how immutable the rest of it is. It compiles to a 13KB binary.

Paired with a udev rule that fixes the hidraw permissions and runs the binary with RUN+= on every connect event, the keyboard now sets itself to function keys automatically, on every reconnect, forever, without a single package added to the host.

Code, the udev rule, and the full writeup are at forge.chach.org/francois/k380-fnlk, MIT-licensed. Credit where due: the HID protocol details (vendor/product ID, the magic report bytes) come from Mike Ribbons’ original Python script, which itself credits k810fn, k380-function-keys-conf, and k380-fn-lock-for-windows for the original reverse-engineering.

I worked through all of this — the toolbox/host permission maze, the BlueZ debugging, the C rewrite — with Claude Code doing the actual typing and a lot of the diagnostic legwork, while I drove the troubleshooting and made the calls on what was worth chasing.