Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

PicoUSBKeyBridge

PicoUSBKeyBridge turns a Raspberry Pi Pico-class board (RP2040/RP2350) into a wired keyboard emulator. It accepts key event packets over UART and exposes a USB HID keyboard to a target host (iPad, macOS, etc).

Related projects

  • keybridged: Go HTTP daemon + client library that sends key events to bridge firmware over UART/USB CDC.
  • NordicBTKeyBridge: Bluetooth Low Energy (BLE) HID version of this idea (USB CDC -> BLE keyboard).

Architecture at a glance

flowchart LR
app[YourApp_or_Scripts] --> http[keybridged_HTTP_API]
http --> uart[UART_via_USB_to_UART]
uart --> fw[PicoUSBKeyBridge_Firmware]
fw --> hid[USB_HID_Keyboard]
hid --> host[TargetHost]
Loading

Use cases:

  • Automate kiosk or demo devices that only accept USB keyboards.
  • Drive hardware test rigs that need deterministic input events.
  • Programmatically emulate keyboard input from software workflows.
  • Remotely send key events to a device without OS-level remote control (e.g., iPhone, iPad).

What you need

  • A Raspberry Pi Pico-class board (RP2040/RP2350).
  • A USB-to-UART serial adapter (3.3V logic).
  • This firmware.

Example hardware

I am using the Waveshare RP2350-USB-A with an FT232 adapter and the default pin mapping. You can, however use your own board and adapter and custom pin mapping.

My setup (annotated)

Note: the Waveshare RP2350-USB-A USB-A port on that board is unused in this firmware; the USB-C port is the HID device port. I originally picked the Waveshare RP2350-USB-A because I wanted to use the Pico-PIO-USB stack. PIO USB failed to enumerate reliably on macOS/iPadOS in my setup (see issue #196), so I moved to TinyUSB over USB-C for HID and used the UART adapter for control/logging.

Build

If you use the example Waveshare RP2350-USB-A board, a prebuilt WaveshareRP2350-USB-A.uf2 is available in the latest release. To build from source:

  1. Install toolchain dependencies: CMake, Ninja, GNU Arm Embedded toolchain (arm-none-eabi-gcc)

  2. Initialize submodules:

git submodule update --init --recursive
  1. Configure:
cmake -S . -B build \
-DPICO_BOARD=waveshare_rp2350_usb_a \
-DPICO_PLATFORM=rp2350-arm-s \
-DPUSBKB_UART_INDEX=1 \
-DPUSBKB_UART_TX_PIN=4 \
-DPUSBKB_UART_RX_PIN=5

Configuration options:

  • PICO_BOARD: Board name (default: waveshare_rp2350_usb_a)
  • PICO_PLATFORM: Platform/chip (default: rp2350-arm-s for RP2350, rp2040 for RP2040)
  • PUSBKB_UART_INDEX: UART instance index (0 or 1, default: 1)
  • PUSBKB_UART_TX_PIN: GPIO pin for UART TX (default: 4)
  • PUSBKB_UART_RX_PIN: GPIO pin for UART RX (default: 5)
  1. Build:
cmake --build build

UF2 output is in build/ (e.g. build/PicoUSBKeyBridge.uf2).

Flash

  1. Hold BOOT and connect the board over USB-C.
  2. A mass-storage device appears (BOOTSEL).
  3. Copy PicoUSBKeyBridge.uf2 to the BOOTSEL drive.

Wiring checklist

  • Connect the board’s USB-C port to the target host (enumerates as a HID keyboard).
  • Connect a USB-to-UART adapter to the board UART pins (for control/logging).
  • Make sure the adapter is set to 3.3V logic.

Default UART wiring (configurable):

  • Adapter RXGPIO4 (UART TX)
  • Adapter TXGPIO5 (UART RX)
  • Adapter GND → any GND on the board

Device identity (serial vs keyboard)

There are two separate identities involved in a typical setup:

  • Serial side (what keybridged uses): the USB VID/PID of the device that provides the serial link from your computer to this firmware.
    • In the example hardware, that’s the FT232 USB-to-UART adapter: VID 0x0403, PID 0x6001.
    • If you use a different adapter, this VID/PID will be different.
    • keybridged defaults are for Nordic USB CDC (VID 0x1915, PID 0x520F), so Pico setups usually need -vid/-pid overrides.
  • Keyboard side (what the target host sees): the USB VID/PID of the Pico’s USB HID keyboard interface.
    • This firmware currently enumerates as VID 0x1915, PID 0xEEEF (values borrowed from Nordic’s HID keyboard samples because they’re a reasonable, known pair).
    • This is unrelated to how keybridged finds the serial device.

Backend daemon (keybridged)

To actually send key events, you usually run the companion daemon on your computer: keybridged.

It keeps a persistent UART connection to the bridge and exposes a small HTTP API.

Example (FT232 adapter):

go run github.com/2opremio/keybridged/cmd/keybridged@latest -vid 0x0403 -pid 0x6001

Quick test (send A = HID code 4 + Shift):

curl -X POST "http://localhost:9876/pressandrelease" \
-H "Content-Type: application/json" \
-d '{"type":"keyboard","code":4,"modifiers":{"left_shift":true}}'

Serial protocol

The UART interface uses a fixed 5-byte packet format. UART is 115200 baud by default.

Packet format (5 bytes)

  • Byte 0: type byte
    • low nibble indicates payload type: 0x00 keyboard, 0x01 consumer control
    • bit 7 set: release event (not set = press event)
  • Byte 1: code low byte
  • Byte 2: code high byte
  • Byte 3: modifier byte (keyboard only, else 0)
  • Byte 4: flags byte (keyboard only, else 0)
    • bit 0: Apple Fn (sets the KeyboardFn byte in the report)

Keyboard payload is code + modifier + flags (keycodes are 8-bit; high byte should be 0).

Consumer payload uses the 16-bit code (little-endian); modifier/flags should be 0.

The code in keyboard payloads uses USB HID keyboard keycodes. They are defined in the HID Usage Tables (Keyboard/Keypad page) and in TinyUSB’s hid.h constants (which may be easier to browse):

Consumer payload codes are USB HID Consumer Page (0x0C) usages (16-bit). They are defined in the HID Usage Tables:

Common consumer usages (hex):

  • Play/Pause: 0x00CD
  • Mute: 0x00E2
  • Volume Increment: 0x00E9
  • Volume Decrement: 0x00EA
  • AL Keyboard Layout (e.g. iPad show/hide keyboard): 0x01AE

Modifier bitmap matches the USB HID keyboard modifier bits (macOS symbols):

  • 0x01 Left Ctrl (⌃)
  • 0x02 Left Shift (⇧)
  • 0x04 Left Alt / Option (⌥)
  • 0x08 Left GUI / Command (⌘)
  • 0x10 Right Ctrl (⌃)
  • 0x20 Right Shift (⇧)
  • 0x40 Right Alt / Option (⌥)
  • 0x80 Right GUI / Command (⌘)

Examples

Press a:

00 04 00 00 00

Release a (release flag set; payload ignored):

80 00 00 00 00

Example A (press then release):

00 04 00 02 00 80 00 00 00 00

Example Fn+K (press then release; Apple Fn flag set):

00 0E 00 00 01 80 00 00 00 00

Example Fn only (press then release; no keycode):

00 00 00 00 01 80 00 00 00 00

Consumer volume increment (usage 0x00E9):

01 E9 00 00 00

UART TX is reserved for logs only. The device never sends protocol bytes back, so the host can safely read TX output as plain text logs.

Porting

The firmware is Raspberry Pi Pico-specific, however, it should be easy to port to other boards using TinyUSB (which is how the HID USB side is implemented).

About

PicoUSBKeyBridge turns a rpi Pico into wired keyboard emulator.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages