DEV Community

Cover image for I built an open-source IP-KVM on a ESP32-P4 (and hit every wall along the way)
Dexif
Dexif

Posted on

I built an open-source IP-KVM on a ESP32-P4 (and hit every wall along the way)

Your home server just stopped booting. No ping, no SSH, dead web UI - and no way to tell whether it's stuck in the BIOS, panicking, or quietly waiting for someone to press F1 after that RAM swap last month. The answer lives on a monitor and keyboard you'd have to physically carry to it.

An IP-KVM fixes exactly this: it captures the machine's HDMI output, presents itself as a USB keyboard and mouse, and puts both in a browser - so you see and drive the box all the way down to its BIOS, from anywhere. The target needs no agent, no OS, not even a working boot: it has no idea its "monitor" and "keyboard" are a small board on the network.

Commercial IP-KVMs cost a fortune. The beloved PiKVM fights the eternal Raspberry Pi shortage. So when Espressif shipped the ESP32-P4 - a chip with a MIPI-CSI camera interface, 32 MB of PSRAM, and USB 2.0 HS - the pieces were suddenly there: a camera input means you can bolt on an HDMI-to-CSI bridge and capture video, for the price of two dev boards and no soldering.

The result is ESP-KVM, fully open source (Apache-2.0). This post is half "here is what it does" and half "here are the walls I ran into," because the second half is the useful part.

GitHub logo espkvm / espkvm

IP-KVM on the ESP32-P4: HDMI capture, USB keyboard and mouse, and a browser console over HTTPS

ESP-KVM

Build Latest release License: Apache-2.0 ESP-IDF v6.0.1 Target: ESP32-P4

Buy me a coffee

An IP-KVM built from an ESP32-P4 and a Toshiba TC358743 HDMI-to-CSI bridge. It captures the target machine's HDMI output, presents itself to that machine as a USB keyboard and mouse, and puts both in a browser.

The point is to reach a machine that has no working operating system - a BIOS screen, a boot menu, a kernel that will not come up - from a device that costs a fraction of a commercial KVM-over-IP.

The ESP-KVM console driving a real machine: its desktop, a right-click menu open on it, the live status bar, and the video settings panel.

Built on jrowny/p4kvm. The hard part - bringing up the TC358743 and getting frames out of the ESP32-P4's CSI receiver - was solved there first, and this project would not exist without it. See Credits.

Status

Useful for what it does today, and honest about the rest.

Video capture, following the target's resolution changes works
MJPEG streaming works
H.264 streaming works; needs HTTPS in the browser (see below)
Keyboard, absolute and relative

Live flasher and interactive demo: espkvm.io.

What it does today

Not a wishlist - this all works right now:

  • Video that follows the target's resolution. BIOS at 640x480, the boot menu, the desktop at 1080p - the capture reconfigures on the fly. Sleep the machine, yank the HDMI, change modes: the stream recovers by itself.
  • Two codecs: MJPEG (simple, ~20 fps) and hardware H.264 (cheap on the wire).
  • Absolute mouse. The click lands on the pixel you aimed at - no acceleration drift, no chasing the cursor. Relative mode is there too for pointer-lock.
  • Full keyboard, media keys, one-button macros (Ctrl+Alt+Del and friends), and type-a-string paste with a keyboard layout.
  • Phone-friendly touch mode: a real virtual trackpad and on-screen keyboard, not a broken desktop layout.
  • Multiple viewers, one in control at a time, with takeover.
  • Virtual media: boot the target from an .iso/.img on a microSD card, or from a small image in the device's own flash.
  • ATX power control (power/reset via optocouplers, power-LED sense) and Wake-on-LAN.
  • HTTPS with a certificate the device issues itself, login, a physical password reset, OTA firmware update with rollback, thermal protection, diagnostics.

two-board sandwich on ribbon cables Waveshare ESP32-P4-ETH + Geekworm C790

The hardware

  • Waveshare ESP32-P4-ETH - ESP32-P4, 32 MB PSRAM, 16 MB flash, 100M Ethernet, microSD, USB OTG, a 40-pin RPi-style header. (product page)
  • Geekworm C790 - an HDMI-to-MIPI-CSI bridge on the Toshiba TC358743, up to 1080p, and it even carries HDMI audio over I2S. (wiki)
  • A MIPI-CSI ribbon between them.

Two USB cables and an HDMI cable, and you are done. No soldering (ATX is the one optional exception, and that's a couple of jumpers into a ready-made optocoupler module).

Credit where it's due

One hard part - bringing up the TC358743 and getting frames out of the P4's CSI receiver - was solved before me by another developer, jrowny/p4kvm. That is weeks of reverse-engineering undocumented MIPI_CSI_BRIDGE registers, and it gave me a working capture layer to start from. I kept that layer and built everything above it (video codecs, transport, web server, security, input, virtual media, power, the whole console UI) from scratch. Attribution lives in the repo, as the license requires.

The walls I hit

This is the part I actually want to write down.

1. Hardware H.264 that got slower than I expected

The plan was obvious: hardware H.264 encoder -> tiny frames -> high frame rate. Reality: on my silicon revision (v1.3) the H.264 block has no color-space converter. The encoder only accepts YUV420 in a specific interleave; the CSI gives you RGB888. So every frame has to pass through a separate hardware converter (the PPA) first - and that pass is bound by PSRAM bandwidth, not compute.

Measured at 1080p: the color conversion alone is ~104 ms, the encode ~45 ms. I overlapped them on two tasks with two buffers, and it barely moved - because the wall is memory, not serialized work. End result is about 7 fps at 1080p over H.264: lovely on bandwidth, painful to watch. So MJPEG (its own ~20 fps) stayed the default, and H.264 is the pick for a slow link.

The RGB-to-YUV converter is gated behind #if CHIP_SUPPORT_MIN_REV >= 300 - it exists on newer silicon, where the PPA pass disappears and the encoder reads RGB directly. Different board, different story. Moral: measure before you celebrate.

2. A 404 that only hit the WebSocket

After adding the ATX endpoints, H.264 and input silently broke in the browser: wss://.../ws returned 404 while every REST route kept working. I chased certificates for an embarrassingly long time before finding it. esp_http_server caps max_uri_handlers, and any handler registered past the cap fails silently - and the video and WebSocket routes register last, so the overflow dropped exactly them. The fix was one line (raise the cap). The trap worth remembering is that nothing warns you: no error, no log, just a route that isn't there. Count your handlers when you add one.

3. microSD that reads single sectors but chokes on the real thing

The card mounted, ls worked, everything looked healthy - and then a USB host reading the disk failed every multi-block read. Which is exactly how a target reads a boot image, so the target could read nothing at all.

At the default 20 MHz the P4's SD interface on this board fails bulk reads; 8 MHz still drops some; input-delay phase tuning did nothing. Dropping to 4 MHz and refusing UHS/DDR made bulk reads clean (verified with a host reading 200 MB, zero errors, ~1.5 MB/s). Slow, but "slow and correct" beats "fast and unreadable." Writes stayed unreliable even there, so the card is served read-only and images are prepared in an external reader. Known ESP32-P4 SD signal-integrity limitation, not my wiring (I keep telling myself).

4. A reset button that killed the network

I wanted a physical "forgot my password" reset on the BOOT button (GPIO 35). Turns out GPIO 35 is shared with the Ethernet interface on this board - claim it after the network is up and the network dies stone dead: the device runs, logs scroll, and it won't answer ARP. It's also a strapping pin, so holding it through a reset lands you in the ROM loader.

The shape the hardware forced: read the button once, in a short window right after app_main starts and before ethernet_init, then hand the pin straight back. And only open that window if a password is actually set - otherwise every boot pays those seconds for nothing.

5. Being your own CA, and PBKDF2 that mbedTLS won't hand you

A browser will not trust a self-signed leaf certificate no matter how you import it - a trust anchor has to be a CA. So the device generates a tiny root CA once, keeps it in NVS, and signs its own server cert with it. You import the CA one time; from then on every cert it serves is trusted, even after the hostname or IP changes. Trusting it also flips on H.264 in the browser, since WebCodecs is a secure-context API.

On the password side, IDF 6 ships mbedTLS 4, where PBKDF2 sits behind a private header, the PSA key-derivation for it isn't compiled in, and the public MD API no longer exposes HMAC. So password hashing meant assembling PBKDF2-HMAC-SHA256 by hand on top of the raw PSA hash - and checking it against a published RFC test vector at boot, because a subtly-wrong KDF still returns bytes, and "wrong" here would mean every password is accepted.

// Refuse to run rather than accept every password
if (!self_test()) {
    ESP_LOGE(TAG, "PBKDF2 self-test failed; authentication is not safe to use");
    return ESP_FAIL;
}
Enter fullscreen mode Exit fullscreen mode

A note on the architecture

One pattern here is worth stealing for any hardware project. Every feature is gated three ways: compile-time (Kconfig, so unused features cost no flash/RAM), probed at boot (each module decides whether the hardware is actually there), and a runtime switch. GET /api/v1/capabilities then reports what is compiled, what was found, what is enabled, and why something is unavailable - so the UI draws a disabled control with a reason ("no SD card") instead of a button that just fails. The web console is Vue 3, embedded as a single gzipped blob, with no external CDN, fonts, or calls out: the device has to work on an isolated network.

Try it / build it / break it

What's next

HDMI audio is the big unbuilt feature (the bridge already emits it over I2S). I'm also designing an enclosure so it stops being a two-board sandwich on ribbon cables, and a new board with fresher P4 silicon and a very different form factor is on its way - the revision where H.264 finally flies. Multi-board support is already baked in for exactly that.

If you have an ESP32-P4 gathering dust, this is a good excuse to use it. And if it saves you a trip to a headless machine in a closet, that was the whole point.

Top comments (0)