{"id":50611402,"url":"https://github.com/0015/esp32-morse-keyer","last_synced_at":"2026-06-06T04:01:45.507Z","repository":{"id":361037022,"uuid":"1248433820","full_name":"0015/esp32-morse-keyer","owner":"0015","description":"An ESP32-based Morse code keyer with LVGL GUI, supporting multiple connectivity options.","archived":false,"fork":false,"pushed_at":"2026-05-25T01:38:12.000Z","size":8567,"stargazers_count":18,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T01:08:17.006Z","etag":null,"topics":["esp32c5","lvgl","mechanicalkeyboard","morse-code","morse-keyboard","screenkey","thatproject"],"latest_commit_sha":null,"homepage":"https://youtu.be/8A1nrHq854I","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0015.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-24T16:29:36.000Z","updated_at":"2026-05-28T17:10:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/0015/esp32-morse-keyer","commit_stats":null,"previous_names":["0015/esp32-morse-keyer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/0015/esp32-morse-keyer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0015%2Fesp32-morse-keyer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0015%2Fesp32-morse-keyer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0015%2Fesp32-morse-keyer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0015%2Fesp32-morse-keyer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0015","download_url":"https://codeload.github.com/0015/esp32-morse-keyer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0015%2Fesp32-morse-keyer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33968711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["esp32c5","lvgl","mechanicalkeyboard","morse-code","morse-keyboard","screenkey","thatproject"],"created_at":"2026-06-06T04:01:45.017Z","updated_at":"2026-06-06T04:01:45.498Z","avatar_url":"https://github.com/0015.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ESP32 Morse Keyer\n\n![Demo](misc/device_demo.gif)\n\nA single-key wireless keyboard built on the **ESP32-C5** that turns Morse code button presses into real keystrokes sent over **Bluetooth Low Energy (BLE) HID** to any paired host — macOS, iOS, or Android.\n\n---\n\n## Why This Exists\n\nStandard HID keyboards require many keys and firmware complexity. This project strips that down to a single physical button. Press the button in short (dot) and long (dash) patterns, and the device decodes the sequence into a character and transmits it as a genuine BLE keyboard report. It serves as a practical exploration of the ESP32-C5's native BLE 5 stack combined with LVGL v9 on a tiny 0.85-inch display, and as a working reference for building a cross-platform BLE HID device from scratch using NimBLE on ESP-IDF 5.5.4.\n\n---\n\n## Features\n\n- **Single-button Morse input** — short press (\u003c 300 ms) = dot, long press (≥ 300 ms) = dash\n- **Auto-decode** — 1 second of silence after the last symbol triggers character decoding (A–Z, 0–9)\n- **BLE HID keyboard** — standard 8-byte HID report, no custom driver needed on any host\n- **Cross-platform** — tested on macOS, iOS, and Android\n- **Secure pairing with bonding** — BLE SM with LE Secure Connections; bond stored in NVS\n- **Bond recovery** — 3 consecutive link-layer failures triggers a 30 s advertising pause, then re-advertises as `MC_Pair` so the host can initiate a clean manual re-pair\n- **Long-press reset** (≥ 3 s) — clears the current Morse sequence, decoded text history, and all stored BLE bonds\n- **Live UI on 0.85-inch display** — shows current Morse symbols being entered (amber), last decoded character (white, 48 px), rolling text history (blue), and a BLE connection indicator dot\n\n---\n\n## Hardware\n\n![Device](misc/device.jpeg)\n\n| Item | Details |\n|---|---|\n| MCU | **ESP32-C5** (RISC-V, single-core 240 MHz, IEEE 802.15.4 + Wi-Fi 6 + BLE 5) |\n| Key module | [Waveshare 0.85inch ScreenKey](https://www.waveshare.com/0.85inch-screenkey.htm) |\n| Display | 0.85-inch IPS, ST7735 driver, 128 × 128, 65K color (RGB565) |\n| Interface | 4-wire SPI2 @ 40 MHz |\n| Button | Integrated mechanical switch (see below) |\n\n### Waveshare ESP32-C5-WiFi6-KIT\n\nThe [Waveshare ESP32-C5-WiFi6-KIT-N16R4](https://www.waveshare.com/esp32-c5-wifi6-kit-n16r4.htm) was chosen as the host board primarily because it exposes an onboard **MX1.25 Li-Po battery connector with a built-in charging IC** — making it trivial to power the keyer from a small 3.7 V lithium battery over USB-C without any external charger circuitry.\n\n| Spec | Value |\n|---|---|\n| SoC | ESP32-C5 (RISC-V, 240 MHz main core + 40 MHz LP core) |\n| Flash / PSRAM | 16 MB SPI Flash + 8 MB PSRAM |\n| Wireless | Dual-band Wi-Fi 6 (2.4 / 5 GHz), BLE 5, Zigbee, Thread |\n| USB | 2× USB Type-C (programming/JTAG + UART up to 3 Mbps) |\n| Battery | 3.7 V MX1.25 Li-Po header + onboard charging IC |\n| Power supply | 5 V via USB-C, 5 V/GND pins, or 3.3 V pins |\n| GPIO | 29 programmable GPIOs (2× 16-pin headers) |\n| Dimensions | 61.4 × 25.4 mm |\n| Extras | RGB LED, boot/reset buttons, charge/power indicator LEDs |\n\n### Waveshare 0.85inch ScreenKey\n\nThe [Waveshare 0.85inch ScreenKey](https://www.waveshare.com/0.85inch-screenkey.htm?\u0026aff_id=116255) is a self-contained keycap module that combines a tiny IPS LCD with a mechanical keyboard switch in a single MX-compatible footprint. It is the physical heart of this project — one module provides both the display and the button.\n\n| Spec | Value |\n|---|---|\n| Display size | 0.85 inch IPS |\n| Resolution | 128 × 128 px |\n| Physical display area | 15.21 × 15.21 mm |\n| Pixel pitch | 118.8 × 118.8 μm |\n| Color depth | 65K (RGB565) |\n| Driver IC | ST7735 |\n| Interface | 4-wire SPI, 3.3 V |\n| Glass | 2.5D tempered, scratch-resistant |\n| Switch actuation force | 50 ± 10 gf |\n| Switch actuation travel | 1.2 ± 0.3 mm |\n| Switch total travel | 2.8 ± 0.25 mm |\n| Switch rated lifespan | 50 million keystrokes |\n\n### Pin Map\n\n| Signal | GPIO |\n|---|---|\n| SPI SCLK | 6 |\n| SPI MOSI | 7 |\n| LCD CS | 8 |\n| LCD DC | 9 |\n| LCD BL | 10 |\n| LCD RST | 26 |\n| Button | 25 |\n\n---\n\n## Software Stack\n\n| Layer | Component |\n|---|---|\n| RTOS | FreeRTOS (via ESP-IDF 5.5.4) |\n| BLE | NimBLE (Apache NimBLE, bundled with ESP-IDF) |\n| HID profile | Custom GATT over NimBLE — HID over GATT Profile (HOGP) |\n| Display driver | `esp_lcd_st7735` + `esp_lcd_panel_ops` |\n| UI framework | **LVGL v9.5.0** via `esp_lvgl_port` |\n| Button | `espressif/button` component |\n| Build system | ESP-IDF 5.5.4, CMake |\n\n---\n\n## BLE 5 / NimBLE Key Functions\n\nThe BLE stack is implemented in [`main/ble_hid/ble_hid_keyboard.c`](main/ble_hid/ble_hid_keyboard.c) using the NimBLE host API.\n\n| Function | Purpose |\n|---|---|\n| `nimble_port_init()` | Initialises the NimBLE host and controller |\n| `nimble_port_freertos_init()` | Starts the NimBLE host task under FreeRTOS |\n| `ble_hs_util_ensure_addr(0)` | Locks the device to its fixed public BLE MAC address — avoids RPA rotation confusion with macOS GATT cache |\n| `ble_svc_gap_init()` / `ble_svc_gatt_init()` | Registers mandatory GAP and GATT services |\n| `ble_svc_bas_init()` | Registers the Battery Service (required by HID profile) |\n| `ble_gatts_count_cfg()` / `ble_gatts_add_svcs()` | Registers the custom HID GATT service and its characteristics |\n| `ble_gap_adv_set_fields()` / `ble_gap_adv_rsp_set_fields()` | Sets advertising payload (HID UUID, appearance 0x03C1) and scan-response (device name) |\n| `ble_gap_adv_start()` | Starts undirected general discoverable advertising |\n| `ble_gap_security_initiate()` | Initiates BLE SM pairing from the peripheral side (required for macOS) |\n| `ble_svc_gatt_changed()` | Sends a GATT Service Changed indication — activates the macOS HID driver on first secure connection |\n| `ble_gatts_notify_custom()` | Sends an 8-byte HID Input Report notification (key press or key release) |\n| `ble_store_util_delete_peer()` | Deletes a single peer's bond when encryption fails (stale LTK recovery) |\n| `ble_store_clear()` | Wipes all stored bonds from NVS (triggered by long-press reset) |\n| `ble_hs_cfg.sm_sc = 1` | Enables LE Secure Connections (LESC) pairing |\n\n### HID Report Format\n\nThe device uses a standard **8-byte keyboard report** with no Report ID prefix, compatible with all three target platforms:\n\n```\nByte 0 : Modifier keys  (0x02 = Left Shift for uppercase letters)\nByte 1 : Reserved       (always 0x00)\nByte 2 : Keycode[0]     (HID usage ID, e.g. 0x04 = A … 0x27 = 0)\nByte 3–7 : Keycode[1–5] (always 0x00 — one key at a time)\n```\n\nA key press sends the report with the keycode populated; a key release sends all zeros 10 ms later.\n\n---\n\n## Project Structure\n\n```\n.\n├── main/\n│   ├── main.c                  # App entry, Morse logic, button handling, LVGL UI\n│   ├── idf_component.yml       # Component dependencies (idf \u003e=5.5.4)\n│   ├── ble_hid/\n│   │   ├── ble_hid_keyboard.c  # BLE HID stack (NimBLE, GATT, SM, advertising)\n│   │   └── ble_hid_keyboard.h  # Public API\n│   ├── lcd_driver/\n│   │   ├── lcd_driver.c        # ST7735 SPI init via esp_lcd\n│   │   └── lcd_driver.h\n│   └── lvgl_driver/\n│       ├── lvgl_driver.c       # LVGL port init and display registration\n│       └── lvgl_driver.h\n├── CMakeLists.txt\n├── partitions.csv\n└── sdkconfig\n```\n\n\n---\n\n## 3D Printed Housing\n\nA custom two-part enclosure was designed for this project. The STL files are in the [`3d_print/`](3d_print/) folder and can be printed with any FDM printer using PLA, ABS, or PETG.\n\n![3D print preview](misc/3d_printing.jpeg)\n\n| File | Description |\n|---|---|\n| [`3d_print/Body_Case.stl`](3d_print/Body_Case.stl) | Main enclosure body — houses the ESP32-C5 dev board and Li-Po battery. Internal mounting posts and side slot cutouts for USB-C access. |\n| [`3d_print/Lid.stl`](3d_print/Lid.stl) | Top lid — square aperture exposes the ScreenKey module flush with the surface. Corner clips snap onto the body. |\n\n---\n\n## Usage\n\n| Action | Result |\n|---|---|\n| Short press (\u003c 300 ms) | Add dot `.` to current sequence |\n| Long press (≥ 300 ms, \u003c 3 s) | Add dash `-` to current sequence |\n| 1 second silence | Decode accumulated sequence → send character via BLE |\n| Press \u0026 hold ≥ 3 s | Clear sequence, history, and all BLE bonds |\n\n### Pairing\n\n1. Power on — device advertises as `Morse_Keyer`\n2. Open Bluetooth settings on host and select `Morse_Keyer`\n3. Confirm pairing — blue dot appears on display when secured\n4. Open any text field and start typing in Morse\n\nTo force a clean re-pair, hold the button for 3 seconds, then forget `Morse_Keyer` on the host before reconnecting.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0015%2Fesp32-morse-keyer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0015%2Fesp32-morse-keyer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0015%2Fesp32-morse-keyer/lists"}