Turn an Adafruit Feather ESP32-S3 (No PSRAM) into a Bluetooth Low Energy HID keyboard that an iPhone can pair with. Text sent to the board over USB serial is typed out to the paired phone as keystrokes.
- Board hardware details: see HARDWARE.md
- Firmware: ble_keyboard/ble_keyboard.ino
- Toolchain + helper scripts: scripts/
- Board identified, hardware/peripherals documented (HARDWARE.md)
- Toolchain installed (arduino-cli 1.5.1 + esp32 core 3.3.10)
- Firmware written: BLE HID keyboard + USB-serial-to-keystroke bridge
- Built with
USBMode=hwcdc(single stable serial port) and flashed - iPhone pairing confirmed — appears as "ESP32-S3 Keyboard", pairs/bonds
- Serial bridge verified — text sent over USB serial is typed to the phone
(tested: "Roster"/"roster" ->
[typed] 7 charsack, appeared in Notes) - LED heartbeat: 3 quick blinks at boot, then fast blink (advertising) / slow blink (connected)
- Pair once on the iPhone: Settings > Bluetooth > "ESP32-S3 Keyboard".
- Focus a text field on the phone (e.g. the Notes app).
- Send text:
powershell -ExecutionPolicy Bypass -File scripts\send.ps1 -Port COM6 -Message "Hello from ESP32!"
scripts\setup.ps1(rebuild toolchain, ~1.5 GB)scripts\build.ps1(compile, uses hwcdc)- If
flash.ps1hits error 31, put board in bootloader (hold BOOT, tap RESET, release BOOT), find its COM port, thenscripts\flash.ps1 -Port COMx - Pair on iPhone, then
scripts\send.ps1 -Port COMx -Message "..."
- The sketch uses only the official ESP32 Arduino core's built-in BLE library
(
BLEHIDDevice) — no third-party libraries. - BLE security is set to Secure Connections + Bonding because iOS only accepts HID keyboard input over an encrypted/bonded link.
loop()reads everything available onSerial(USB) and types it to the phone, pressing Enter on newline (configurable viaAPPEND_ENTER_ON_NEWLINE).- The LED is a heartbeat so you can see the program is alive.
| Component | Version | Source |
|---|---|---|
| arduino-cli | 1.5.1 | downloads.arduino.cc (installed to %USERPROFILE%\tools) |
| ESP32 Arduino core | esp32:esp32@3.3.10 |
board manager URL below |
| Board FQBN | esp32:esp32:adafruit_feather_esp32s3_nopsram:USBMode=hwcdc |
|
| Board manager URL | https://espressif.github.io/arduino-esp32/package_esp32_index.json |
setup.ps1 installs arduino-cli per-user (no admin needed). The core download is
~1.5 GB and the first compile builds the whole framework from source (this took
~30 min on the original, slower machine; the new machine with admin + more cores
should be faster). Subsequent compiles are cached and take seconds.
- Native USB, no separate USB-serial chip. The ESP32-S3 USB behaves very differently from classic ESP32 dev boards.
- Bootloader entry for flashing: auto-reset is unreliable. If
flash.ps1fails withPermissionError(13 ... not functioning)(Windows error 31), enter the ROM bootloader manually: hold BOOT, tap RESET, release BOOT. The board then enumerates as native USB-Serial/JTAG (VID 303A), often on a new COM number. - Two USB identities:
- App firmware (Adafruit factory image) showed up as VID 239A / PID 8113.
- ROM bootloader / hardware USB-Serial/JTAG shows up as VID 303A / PID 1001.
- Serial open must NOT assert DTR/RTS — on the hardware USB-Serial/JTAG those
lines are wired to reset/boot.
send.ps1opens with both de-asserted. - Opening the port can still reset the chip (
rst:0x15 USB_UART_CHIP_RESET), which briefly drops the BLE link.send.ps1therefore waits ~5 s after opening for the phone to auto-reconnect, and retries if the firmware reports[skip]. USBMode=hwcdc+Serial.setTxTimeoutMs(0)are both required. Without the non-blocking TX timeout,Serial.print()insetup()blocks forever when no terminal is attached and the firmware never reachesloop()(no LED, no BLE).- If the board seems dead (no LED, not advertising) after lots of flashing/ serial poking, it may be wedged in ROM download mode — unplug/replug to get a clean power-on boot.
- ESP32-S3 is BLE-only (no Bluetooth Classic). iOS pairs with BLE HID fine.
- Core 3.x note: the old
BLE2902descriptor class was removed; the sketch does not use it (CCCD is handled automatically byBLEHIDDevice).
A BLE keyboard types wherever the phone's cursor is — it does not "send" a message
on its own. Focus a text field first (Notes is easiest). Newline triggers Enter, so
send.ps1 -Port COMx -Message "hi" types hi then Enter. Use -NoEnter to skip
the Enter (e.g. when typing into a search box you don't want to submit).