If you want to know how to pair a Pico remote to a PC, smartphone, or tablet, the most reliable method is configuring the Raspberry Pi Pico W as a Bluetooth Low Energy (BLE) Human Interface Device (HID). Unlike proprietary 2.4GHz RF remotes that require a dedicated USB dongle, a BLE HID remote pairs natively with Windows, macOS, iOS, and Android using the host device’s built-in Bluetooth stack.

This guide walks through the exact hardware requirements, the MicroPython code to emulate a BLE keyboard, and the specific debugging steps required when the Cypress CYW43439 wireless chip throws synchronization errors during the pairing process.

Project Overview & Hardware Requirements

Building a wireless remote requires managing both the logical BLE stack and the physical power delivery. The Raspberry Pi Pico W is the target board variant for this build because it integrates the RP2040 microcontroller with the Infineon CYW43439 WiFi/BLE coprocessor on a single module.

Difficulty & Time Rating

Difficulty: Intermediate (Requires MicroPython BLE stack knowledge and basic soldering)
Time to Build: 2 hours (Hardware assembly) + 1 hour (Code deployment and OS pairing)
Estimated Cost: $12 - $18 USD

Parts List

  • Microcontroller: Raspberry Pi Pico W (with pre-soldered pin headers). Do not use the standard Pico or Pico 2; they lack the BLE radio.
  • Switches: 4x 6x6x5mm Tactile Pushbuttons (Through-hole).
  • Power: 3.7V 500mAh 1S LiPo Battery (e.g., Adafruit 3898 or equivalent with JST-PH 2.0 connector).
  • Charging: TP4056 LiPo charging module (must include the DW01A protection IC to prevent over-discharge).
  • Substrate: 5x7cm Perfboard or custom PCB.
⚠️ Lithium Fire Safety: Never wire a raw LiPo cell directly to the Pico W VSYS pin without a protection circuit. If the battery voltage drops below 2.8V, the cell can suffer copper dendrite growth and short-circuit during the next charge cycle. Always use a TP4056 module with integrated DW01A protection.

Pico W Pin Mapping & RF Power Specifications

Before wiring the tactile switches, you must understand the power constraints of the Pico W. The CYW43439 chip draws significant peak current during BLE advertising and transmission. If your power supply cannot deliver this current, the radio will brownout and fail to initialize.

Pin / Net Function Voltage / Logic Constraints & Notes
GP10 Button 1 (Input) 3.3V Logic (Active Low) Enable internal pull-up in code. Debounce required.
GP11 Button 2 (Input) 3.3V Logic (Active Low) Enable internal pull-up in code.
GP12 Button 3 (Input) 3.3V Logic (Active Low) Enable internal pull-up in code.
GP13 Button 4 (Input) 3.3V Logic (Active Low) Enable internal pull-up in code.
VSYS (Pin 39) Main Power Input 1.8V to 5.5V Connect LiPo + via TP4056 OUT+. Minimum 3.3V required for stable BLE TX.
3V3(OUT) (Pin 36) Internal Regulator Out 3.3V Do NOT draw more than 300mA total from this pin. CYW43439 draws from here internally.
GND (Pin 38) Common Ground 0V Must share common ground with TP4056 and buttons.
CYW43439 TX Peak RF Transmission N/A (Current Draw) Peak: ~130mA during BLE packet transmission. Ensure LiPo ESR is low.

For authoritative details on the Pico W power tree, refer to the Raspberry Pi Pico W Datasheet, specifically Section 3.4 regarding the CYW43439 power domains.

Step-by-Step: Flashing and Pairing the Pico Remote

Pairing a BLE HID device is not the same as pairing a standard serial Bluetooth module. The host OS expects a specific GATT (Generic Attribute Profile) service tree. Follow these steps to establish the bond.

  1. Flash the Firmware: Download the latest stable MicroPython UF2 specifically built for the Raspberry Pi Pico W from the official MicroPython download page. Hold the BOOTSEL button, plug in USB, and drag the UF2 file to the RPI-RP2 drive.
  2. Upload the Code: Use Thonny or VS Code (with the MicroPython extension) to upload the complete HID script (provided below) as main.py on the Pico W.
  3. Power Cycle: Disconnect USB and plug in your LiPo battery. The onboard LED will pulse slowly, indicating the BLE stack is advertising.
  4. Initiate Host Pairing (Windows 11): Go to Settings > Bluetooth & devices > Add device > Bluetooth. Look for Pico-HID-Remote.
  5. Complete the Bond: Click the device. Windows may prompt you to type a PIN on the remote. Because this is a custom macro remote, Windows will usually bypass the PIN prompt and show "Connected". If it asks for a PIN, click "Cancel" or "Try connecting without a PIN" (Windows 11 allows this for HID devices without keyboards).
  6. Verify: Open a text editor on your PC. Press Button 1 on the Pico remote. The letter 'A' should appear.

Complete MicroPython BLE HID Code

This code targets the Raspberry Pi Pico W. It uses the native bluetooth module to register a standard Keyboard HID service. It includes explicit pin definitions, a standard 54-byte HID report descriptor, and error handling for the CYW43439 SPI sync failures.


import bluetooth
import struct
import time
from machine import Pin, Timer

# --- PIN DEFINITIONS ---
BUTTON_PINS = [10, 11, 12, 13]
LED_PIN = 25  # Note: On Pico W, GPIO 25 is the CYW43439 WL_GPIO0 pin, not the physical board LED.
# We use the physical LED via the wireless chip if needed, but keep it simple here.

# --- BLE HID DESCRIPTOR (Standard Keyboard) ---
HID_REPORT_DESCRIPTOR = bytes((
    0x05, 0x01,  # Usage Page (Generic Desktop Ctrls)
    0x09, 0x06,  # Usage (Keyboard)
    0xA1, 0x01,  # Collection (Application)
    0x85, 0x01,  #   Report ID (1)
    0x05, 0x07,  #   Usage Page (Kbrd/Keypad)
    0x19, 0xE0,  #   Usage Minimum (0xE0)
    0x29, 0xE7,  #   Usage Maximum (0xE7)
    0x15, 0x00,  #   Logical Minimum (0)
    0x25, 0x01,  #   Logical Maximum (1)
    0x75, 0x01,  #   Report Size (1)
    0x95, 0x08,  #   Report Count (8)
    0x81, 0x02,  #   Input (Data,Var,Abs)
    0x95, 0x01,  #   Report Count (1)
    0x75, 0x08,  #   Report Size (8)
    0x81, 0x01,  #   Input (Const)
    0x95, 0x06,  #   Report Count (6)
    0x75, 0x08,  #   Report Size (8)
    0x15, 0x00,  #   Logical Minimum (0)
    0x25, 0x65,  #   Logical Maximum (101)
    0x05, 0x07,  #   Usage Page (Kbrd/Keypad)
    0x19, 0x00,  #   Usage Minimum (0x00)
    0x29, 0x65,  #   Usage Maximum (0x65)
    0x81, 0x00,  #   Input (Data,Array)
    0xC0         # End Collection
))

# Bluetooth SIG UUIDs
UUID_HID_SERVICE = bluetooth.UUID(0x1812)
UUID_HID_REPORT = bluetooth.UUID(0x2A4D)
UUID_HID_INFO = bluetooth.UUID(0x2A4A)

class PicoHIDRemote:
    def __init__(self, ble):
        self._ble = ble
        self._ble.active(1)
        self._ble.irq(self._irq)
        self._setup_services()
        self._connections = set()
        self._buttons = [Pin(p, Pin.IN, Pin.PULL_UP) for p in BUTTON_PINS]
        self._last_state = [1, 1, 1, 1]
        
    def _setup_services(self):
        # HID Information: 0x00C1 (Keyboard), 0x00 (Country), 0x02 (Normally Connectable)
        hid_info = struct.pack('

Debugging: Pairing Failures & "ETIMEDOUT" Errors

The CYW43439 coprocessor communicates with the RP2040 over an internal SPI bus. When the BLE stack fails to initialize or the host OS refuses to pair, the errors are often cryptic.

The First Three Things to Check

  1. Firmware Variant: Verify you flashed the firmware.uf2 specifically named for the Pico W. The standard Pico firmware lacks the CYW43439 driver and will throw an immediate AttributeError: module 'bluetooth' has no attribute 'BLE'.
  2. Host MAC Randomization: Windows 11 and iOS use randomized MAC addresses for BLE scanning. If your Pico was previously bonded to the PC, delete the old device from the Windows Bluetooth menu. Stale bonding keys will cause the OS to silently drop the connection.
  3. 3.3V Rail Brownout: If the Pico resets exactly when you click "Pair" on your PC, the CYW43439 is pulling 130mA+ during the pairing handshake, causing a voltage drop on the 3.3V LDO. Add a 100µF ceramic capacitor directly across the VSYS and GND pins.

Exact Error Strings & Ranked Causes

Error String: OSError: [Errno 110] ETIMEDOUT during ble.active(1) or gap_advertise().

Ranked Causes:

  1. SPI Bus Contention (Most Likely): You have external SPI devices (like an OLED display or SD card) wired to the default Pico SPI0 pins (GP16-GP19). The CYW43439 uses a dedicated internal SPI bus, but user code initializing external SPI without proper CS pin management can lock the RP2040's DMA or SPI peripherals. Fix: Move external SPI devices to SPI1 (GP10-GP13 are safe for buttons, use GP8-GP11 for SPI1 if needed, adjusting button pins accordingly).
  2. WiFi/BLE Mutex Lock: If you are running WiFi (e.g., NTP sync) concurrently with BLE without using the aioble async library, the CYW43439 driver will block. Fix: Disable WiFi entirely for this build, or migrate to async aioble.
  3. Thermal Throttling: The RP2040 internal flash or the CYW43439 is overheating due to a shorted pin. Fix: Measure current draw; it should be <25mA at idle.
Error String: Windows shows "Couldn't connect. Try again." or "Bluetooth device not authenticated" immediately after clicking Pair.

Ranked Causes:

  1. Missing HID Report Map: The host OS queried the GATT table, found the HID Service (0x1812), but the Report Map characteristic (0x2A4B) was malformed or missing. Windows rejects HID devices without a valid descriptor. Fix: Ensure the 54-byte descriptor array in the code above is copied exactly.
  2. Advertising Interval Too High: The Pico is advertising at 100ms intervals, but Windows expects faster bursts during the initial pairing window. Fix: Change gap_advertise(100000) to gap_advertise(40000) (40ms) for the first 10 seconds after boot.

Extending the Build: Joysticks vs. Simplifying for Power

Once you have the base HID remote paired and functioning, you will likely want to modify the input method or optimize the battery life.

How to Extend: Adding Analog Joysticks

To convert this from a macro pad into a game controller, replace the tactile switches with dual-axis analog joysticks (e.g., PS2-style thumbsticks).

  • Wiring: Connect the X and Y potentiometers to the Pico W’s ADC pins (GP26, GP27, GP28). Do not use GP29 if you are reading VSYS voltage for battery monitoring.
  • Code Modification: Change the HID Report Descriptor from a "Keyboard" to a "Gamepad" (Usage Page 0x01, Usage 0x05). You will need to map the 12-bit ADC values (0-4095) to the 8-bit signed integer range (-127 to 127) expected by the HID gamepad report format.
  • Deadzone Handling: Analog sticks drift. Implement a software deadzone in MicroPython: if abs(raw_value - 2048) < 200: raw_value = 2048.

How to Simplify: Dropping BLE for ESP-NOW

If you are building a remote to control another microcontroller (like a robot or a smart light) and do not need to pair with a PC or Phone, drop the BLE HID stack entirely.

  • The BLE HID stack consumes roughly 15mA continuously while advertising and managing GATT connections.
  • Instead, use ESP-NOW (if you switch the receiver to an ESP32) or a raw nRF24L01+ module wired to the Pico W's SPI1 bus. ESP-NOW allows for sub-millisecond latency and drops the idle current to under 2mA between packet bursts, extending a 500mAh LiPo battery life from 2 days to over 3 weeks.

For deeper reference on BLE GATT specifications and HID descriptors, consult the Bluetooth SIG HID Service Specification and the MicroPython Bluetooth Documentation.