NetHID started as a small itch: I wanted to type into a machine from across the room without a physical keyboard plugged into it, and without installing anything on that machine. The trick is that a computer will happily trust a USB keyboard it’s never met — no drivers, no pairing, no permission prompt. So the plan was simple: build a device that is a USB keyboard and mouse to whatever it’s plugged into, but takes its orders over Wi-Fi from somewhere else.

The whole thing runs on a Raspberry Pi Pico 2 W — the RP2350 board with wireless — and costs about the price of a sandwich.

The two-machine idea

The key mental model is that there are always two machines involved, and they’re not the same one. One is the target: whatever the Pico’s USB cable plugs into. To that machine, NetHID is an ordinary composite HID device — a keyboard, a relative mouse, and an absolute pointer. The other is the controller: my laptop or phone, talking to the Pico over the network. Keystrokes and clicks originate on the controller, fly over Wi-Fi to the Pico, and come out the USB port as real HID reports on the target.

That separation is what makes it useful. The target needs zero software and grants zero permissions; it just sees a keyboard.

Talking to it

There are a few ways to drive the Pico, and I ended up building all of them because each earns its place:

The socket is what makes the most fun feature possible: a little pynput-based capture tool that grabs my Mac’s own keyboard and mouse and forwards them live. Point it at the Pico and my laptop becomes a software KVM head — whatever I type and wherever I move the pointer happens on the target machine instead. Holds, modifiers, chords, drags: all mirrored as real HID reports, not just tap-at-a-time macros.

TLS on a microcontroller

The part I’m most quietly proud of is that the Pico terminates HTTPS itself. There’s no reverse proxy, no companion server — mbedTLS runs on the RP2350, and the web UI and API are served over https:// straight from the chip, with a second TLS listener on the control socket too. Encryption runs all the way to the device.

Getting there was not smooth. For the longest time the TLS handshake would reset the instant the client hello arrived, with mbedTLS producing no debug output at all — as if it were never invoked. It turned out the connection pool for lwIP’s TLS layer was one entry too small: a TLS listener quietly needs two protocol control blocks, and each accepted connection needs two more. The pool ran dry, an allocation returned NULL silently, and the stack tore the connection down before TLS ever got a look in. One number in a config header. Two days of my life. The moment the pool was big enough, a full TLS 1.2 handshake sprang to life on the first try.

Auth is HMAC challenge-response over the JSON path — the password never crosses the wire — and the binary socket rides inside TLS for confidentiality.

Blasting remotes

Somewhere along the way NetHID grew a second personality: it can also be a universal remote. The RP2350’s PIO blocks are perfect for bit-banging precise timing without the CPU jittering the signal, so the device drives an IR LED (38 kHz carrier, through a transistor) to impersonate a TV remote, and a 433 MHz OOK transmitter for the cheap RF gear that runs things like projector screens and plug sockets. It can learn, too — point a remote at a receiver and it captures the raw timing, ready to replay.

The learning side taught me humility about noise. A 433 MHz receiver with no signal doesn’t sit quiet; its automatic gain control cranks up and the data line chatters constantly. My first “successful” capture was pure garbage that happened to look frame-shaped. The fix was to stop trusting any single capture and instead demand consensus — press the button a few times, split each capture on its sync gaps, and only accept a frame that repeats identically. Real fixed codes repeat; noise doesn’t; rolling codes repeat differently every time and tell you honestly that they can’t be replayed.

The bug that only macOS could see

My favourite gremlin: for ages, mouse clicks did nothing on my Mac while the keyboard worked perfectly. The movement works, as does clicking the absolute positioned mouse. I suspect the culprit to be either a HID report descriptor that didn’t quite match the bytes the firmware sent — the sort of tiny inconsistency Windows shrugs off and macOS refuses outright, dropping the offending reports on the floor, or there is an issue with having two pointing devices on separate pages but in one report. It’s a good reminder that “works on my machine” is doing a lot of heavy lifting when your machine is the forgiving one. USB hosts have opinions, and they don’t all share them.

What it is now

NetHID is a Pico that pretends to be a keyboard, a mouse, a TV remote, and a garage-door fob, all controllable from a browser, a script, or a live capture of my own input — over an encrypted link the microcontroller secures by itself. It’s a toy in the best sense: small, self-contained, and endlessly pokeable. Half the value has been the debugging — undersized buffers, silent NULLs, invisible infrared, and a host OS with strong feelings about descriptors.

If nothing else, it’s made me suspicious of every “it just works” USB device I own. They all had to fight these fights too.