Building a Raspberry Pi KVM (Keyboard, Video, Mouse) switch gives you out-of-band, IPMI-style control over headless servers, network switches, or crashed desktops for a fraction of a commercial IP-KVM's cost. By leveraging the Pi's native USB OTG (On-The-Go) gadget mode and a cheap UVC HDMI capture card, you can achieve 1080p60 video capture and virtual HID emulation with sub-50ms latency. You can build a fully functional DIY Raspberry Pi KVM for about $95 using a Pi 4, a MacroSilicon capture module, and a custom ATX control bridge.

This guide provides the exact hardware spec sheet, GPIO pin mapping for ATX power control, a complete Python daemon for remote power cycling, and the specific debugging steps for the most common USB OTG failures.

DIY Raspberry Pi KVM Hardware Spec Sheet & Parts List

The foundation of a reliable Raspberry Pi KVM build is selecting components that support native UVC (USB Video Class) and standard USB HID protocols without requiring proprietary drivers on the target host. Below is the exact parts list and specification matrix for a robust v3-style DIY build.

Component Exact Model / Variant Function & Interface Est. Cost
Compute Module Raspberry Pi 4 Model B (4GB) Main brain; handles USB OTG gadget, video encoding, and GPIO. (Pi 5 lacks native USB-C OTG without adapter boards). $55.00
Video Capture MacroSilicon MS2130 (USB 3.0) HDMI to UVC capture. Supports 1080p60 MJPEG/YUYV. Low latency, no drivers needed on target. $18.00
USB Hub FE1.1s 4-Port USB 2.0 Module Splits the Pi's OTG port to emulate multiple devices (Keyboard, Mouse, Mass Storage, Serial). $6.00
ATX Control PC817 Optocoupler Module (4-ch) Isolates Pi 3.3V GPIO from target motherboard 5V/3.3V ATX front panel headers to prevent ground loops. $4.00
Cabling & Misc USB-C to USB-A Data Cable + Micro-HDMI Must be a data-capable USB-C cable for OTG. Micro-HDMI to HDMI for capture input. $12.00

Bench Note: Avoid the Elgato Cam Link for DIY Pi KVM builds. While it's a great capture card for streaming, it enumerates as a specific vendor ID that some BIOS/UEFI environments reject when paired with a virtual USB hub. The MS2130 or Auvidea B101 (CSI-based) are vastly more compatible with pre-boot environments.

Pin Mapping & Wiring the ATX Control Bridge

To remotely power cycle the target machine, the Pi must interface with the motherboard's front panel headers. Never wire the Pi's GPIO directly to the ATX power switch pins. Motherboard standoffs and PSU ground differentials can introduce voltage spikes that will fry the Pi's BCM chip. Always use optocouplers or a dedicated relay HAT.

The following mapping targets the Raspberry Pi 4 Model B (4GB) using BCM pin numbering.

Pi GPIO (BCM) Optocoupler / Relay Input Target Motherboard Header Direction
GPIO 25 Channel 1 IN ATX Power SW (PWR_BTN#) Output (Active Low)
GPIO 24 Channel 2 IN ATX Reset SW (RESET#) Output (Active Low)
GPIO 22 Channel 3 OUT ATX Power LED (+) Input (Read 3.3V/5V)
GPIO 23 Channel 4 OUT ATX HDD LED (+) Input (Read 3.3V/5V)
3.3V Power VCC (Input side) N/A Power
GND GND (Input side) Common Ground Ground

For serial console access (useful for debugging Linux kernel panics on the target), wire Pi GPIO 14 (TX) to the Target UART RX, and Pi GPIO 15 (RX) to Target UART TX. Remember to cross the lines (TX to RX, RX to TX) and ensure both devices share a common ground.

Python ATX Control & Serial Console Code

While the official PiKVM OS handles this via its built-in kvm service, building a custom lightweight Python daemon gives you bare-metal control for headless automation. The following script uses gpiozero and Python's built-in http.server to expose a local REST API for triggering the ATX power and reset switches.

Target Board: Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (64-bit, Bookworm).

import http.server
import socketserver
import json
import logging
import time
from gpiozero import LED, Button
from signal import pause

# Configure Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Pin Definitions (BCM numbering)
PIN_ATX_POWER = 25
PIN_ATX_RESET = 24
PIN_ATX_POWER_LED = 22
PIN_ATX_HDD_LED = 23

# Initialize GPIO
# Using LED class for output to easily toggle/pulse
power_sw = LED(PIN_ATX_POWER)
reset_sw = LED(PIN_ATX_RESET)

# Using Button class for input with pull-down resistors
power_led = Button(PIN_ATX_POWER_LED, pull_up=False)
hdd_led = Button(PIN_ATX_HDD_LED, pull_up=False)

def press_button(switch, duration=0.2):
    """Simulates a human pressing an ATX button."""
    switch.on()
    time.sleep(duration)
    switch.off()

class KVMHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            if self.path == '/api/status':
                status = {
                    'power_led': power_led.is_pressed,
                    'hdd_led': hdd_led.is_pressed
                }
                self.send_response(200)
                self.send_header('Content-type', 'application/json')
                self.end_headers()
                self.wfile.write(json.dumps(status).encode('utf-8'))
                
            elif self.path == '/api/power':
                logging.info('Triggering ATX Power Button')
                press_button(power_sw, duration=0.2)
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b'Power button pressed')
                
            elif self.path == '/api/reset':
                logging.info('Triggering ATX Reset Button')
                press_button(reset_sw, duration=0.2)
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b'Reset button pressed')
                
            elif self.path == '/api/hard-reset':
                # Hold power button for 5 seconds to force ACPI shutdown
                logging.warning('Triggering Hard Reset (5s hold)')
                press_button(power_sw, duration=5.0)
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b'Hard reset executed')
            else:
                self.send_error(404, 'Endpoint not found')
                
        except Exception as e:
            logging.error(f'API Error: {str(e)}')
            self.send_error(500, f'Internal Server Error: {str(e)}')

    def log_message(self, format, *args):
        # Suppress default verbose HTTP logs, rely on our logging module
        pass

if __name__ == '__main__':
    PORT = 8080
    with socketserver.TCPServer(('', PORT), KVMHandler) as httpd:
        logging.info(f'Pi KVM ATX Control API listening on port {PORT}')
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            logging.info('Shutting down ATX Control API')
            httpd.shutdown()

Save this as atx_control.py and run it via systemd to ensure it starts on boot. You can now trigger a power-on by sending a GET request to http://<pi-ip>:8080/api/power.

Debugging: USB OTG Dropouts and Capture Failures

The most notorious point of failure in any Raspberry Pi KVM build is the USB OTG gadget connection. When the target host fails to enumerate the Pi as a keyboard/mouse, or the video feed stutters, you will typically encounter the following error in your dmesg or PiKVM service logs:

libusb: error [submit_bulk_transfer] submiturb failed error -1 errno=2

This exact error string indicates that the Linux kernel's USB subsystem failed to submit a bulk transfer request to the host controller. It is almost always a physical layer or driver configuration issue, not a software bug in your Python code.

The First Three Things to Check When It Fails

  1. Verify the USB-C Cable has Data Lines: Over 60% of 'dead' Pi KVM builds are caused by using a charge-only USB-C cable. A charge-only cable lacks the D+ and D- data pins. Test the cable by plugging the Pi into a standard PC; if the PC doesn't chime and show a new 'USB Composite Device' or 'Mass Storage' device in Device Manager, your cable is the culprit. Swap it for a verified data cable.
  2. Confirm the DWC2 Overlay is Active: The Pi 4 requires the DesignWare Core USB 2.0 driver to act as a device. SSH into the Pi and run lsmod | grep dwc2. If it returns nothing, your /boot/config.txt is missing the dtoverlay=dwc2 directive. Add it under the [all] section and reboot.
  3. Check UVC Capture Enumeration: If the keyboard works but the video is black, the capture card might be failing to initialize. Run v4l2-ctl --list-devices. You should see 'USB Video' or 'MS2130' mapped to a /dev/videoX node. If it's missing, the MS2130 module is either dead, unseated, or drawing too much current from the Pi's USB bus (requiring a powered hub).

Power Budget Warning: The Pi 4's USB bus is limited to 1.2A total. The MS2130 capture card can draw up to 500mA during 1080p60 MJPEG encoding. If you are also powering a FE1.1s hub and an optocoupler board, you may hit the brownout threshold. Use a high-quality 5V/3A (or 5.1V/3.5A) USB-C power supply for the Pi itself to prevent peripheral dropouts.

Extending and Simplifying the Build

Once you have the baseline Raspberry Pi KVM running, you can tailor the complexity to your specific lab or server rack needs.

How to Simplify the Build

If writing custom Python daemons and configuring libcomposite USB gadget scripts sounds like unnecessary friction, flash the official PiKVM OS. The PiKVM project provides a pre-built, read-only root filesystem image that handles the Janus WebRTC video streaming, USB OTG HID emulation, and ATX GPIO mapping out of the box. You simply wire the hardware according to their v3 HAT schematic, flash the SD card, and access the web interface. This reduces a 4-hour weekend project to a 20-minute assembly.

How to Extend the Build

  • Add an I2C OLED Status Display: Wire a 0.96-inch SSD1306 OLED to the Pi's I2C pins (GPIO 2/3). Use the luma.oled Python library to display the Pi's current IP address, CPU temperature, and the target host's serial console output directly on the KVM chassis. This is invaluable when you plug the KVM into a new network and need to find its DHCP lease without a monitor.
  • Implement a Hard Power Cut Relay: Optocouplers can only simulate a button press. If the target's power supply is completely locked up, a button press won't help. Add a 5V relay module controlled by GPIO 26 to physically switch the 120V/240V AC line feeding the target's PSU. Safety Note: Only attempt mains voltage switching if you are experienced with AC wiring, use a properly rated relay (e.g., Omron G5LE), and enclose it in a grounded, fire-rated junction box.
  • Virtual Media Boot: By configuring the mass_storage gadget function in Linux, you can mount an ISO file (like an Ubuntu Server installer or a Windows PE rescue disk) from the Pi's SD card and present it to the target as a bootable USB drive. This allows you to remotely reinstall the OS on a completely bricked machine without ever plugging in a physical thumb drive.

Building your own Raspberry Pi KVM bridges the gap between expensive enterprise IPMI solutions and cheap, dumb power strips. By verifying your USB-C data lines, isolating your ATX GPIO with optocouplers, and handling the UVC capture stack correctly, you gain total remote sovereignty over your hardware stack.