An open API service indexing awesome lists of open source software.

https://github.com/0015/esp32-morse-keyer

An ESP32-based Morse code keyer with LVGL GUI, supporting multiple connectivity options.
https://github.com/0015/esp32-morse-keyer

esp32c5 lvgl mechanicalkeyboard morse-code morse-keyboard screenkey thatproject

Last synced: 2 days ago
JSON representation

An ESP32-based Morse code keyer with LVGL GUI, supporting multiple connectivity options.

Awesome Lists containing this project

README

          

# ESP32 Morse Keyer

![Demo](misc/device_demo.gif)

A 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.

---

## Why This Exists

Standard 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.

---

## Features

- **Single-button Morse input** — short press (< 300 ms) = dot, long press (≥ 300 ms) = dash
- **Auto-decode** — 1 second of silence after the last symbol triggers character decoding (A–Z, 0–9)
- **BLE HID keyboard** — standard 8-byte HID report, no custom driver needed on any host
- **Cross-platform** — tested on macOS, iOS, and Android
- **Secure pairing with bonding** — BLE SM with LE Secure Connections; bond stored in NVS
- **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
- **Long-press reset** (≥ 3 s) — clears the current Morse sequence, decoded text history, and all stored BLE bonds
- **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

---

## Hardware

![Device](misc/device.jpeg)

| Item | Details |
|---|---|
| MCU | **ESP32-C5** (RISC-V, single-core 240 MHz, IEEE 802.15.4 + Wi-Fi 6 + BLE 5) |
| Key module | [Waveshare 0.85inch ScreenKey](https://www.waveshare.com/0.85inch-screenkey.htm) |
| Display | 0.85-inch IPS, ST7735 driver, 128 × 128, 65K color (RGB565) |
| Interface | 4-wire SPI2 @ 40 MHz |
| Button | Integrated mechanical switch (see below) |

### Waveshare ESP32-C5-WiFi6-KIT

The [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.

| Spec | Value |
|---|---|
| SoC | ESP32-C5 (RISC-V, 240 MHz main core + 40 MHz LP core) |
| Flash / PSRAM | 16 MB SPI Flash + 8 MB PSRAM |
| Wireless | Dual-band Wi-Fi 6 (2.4 / 5 GHz), BLE 5, Zigbee, Thread |
| USB | 2× USB Type-C (programming/JTAG + UART up to 3 Mbps) |
| Battery | 3.7 V MX1.25 Li-Po header + onboard charging IC |
| Power supply | 5 V via USB-C, 5 V/GND pins, or 3.3 V pins |
| GPIO | 29 programmable GPIOs (2× 16-pin headers) |
| Dimensions | 61.4 × 25.4 mm |
| Extras | RGB LED, boot/reset buttons, charge/power indicator LEDs |

### Waveshare 0.85inch ScreenKey

The [Waveshare 0.85inch ScreenKey](https://www.waveshare.com/0.85inch-screenkey.htm?&aff_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.

| Spec | Value |
|---|---|
| Display size | 0.85 inch IPS |
| Resolution | 128 × 128 px |
| Physical display area | 15.21 × 15.21 mm |
| Pixel pitch | 118.8 × 118.8 μm |
| Color depth | 65K (RGB565) |
| Driver IC | ST7735 |
| Interface | 4-wire SPI, 3.3 V |
| Glass | 2.5D tempered, scratch-resistant |
| Switch actuation force | 50 ± 10 gf |
| Switch actuation travel | 1.2 ± 0.3 mm |
| Switch total travel | 2.8 ± 0.25 mm |
| Switch rated lifespan | 50 million keystrokes |

### Pin Map

| Signal | GPIO |
|---|---|
| SPI SCLK | 6 |
| SPI MOSI | 7 |
| LCD CS | 8 |
| LCD DC | 9 |
| LCD BL | 10 |
| LCD RST | 26 |
| Button | 25 |

---

## Software Stack

| Layer | Component |
|---|---|
| RTOS | FreeRTOS (via ESP-IDF 5.5.4) |
| BLE | NimBLE (Apache NimBLE, bundled with ESP-IDF) |
| HID profile | Custom GATT over NimBLE — HID over GATT Profile (HOGP) |
| Display driver | `esp_lcd_st7735` + `esp_lcd_panel_ops` |
| UI framework | **LVGL v9.5.0** via `esp_lvgl_port` |
| Button | `espressif/button` component |
| Build system | ESP-IDF 5.5.4, CMake |

---

## BLE 5 / NimBLE Key Functions

The BLE stack is implemented in [`main/ble_hid/ble_hid_keyboard.c`](main/ble_hid/ble_hid_keyboard.c) using the NimBLE host API.

| Function | Purpose |
|---|---|
| `nimble_port_init()` | Initialises the NimBLE host and controller |
| `nimble_port_freertos_init()` | Starts the NimBLE host task under FreeRTOS |
| `ble_hs_util_ensure_addr(0)` | Locks the device to its fixed public BLE MAC address — avoids RPA rotation confusion with macOS GATT cache |
| `ble_svc_gap_init()` / `ble_svc_gatt_init()` | Registers mandatory GAP and GATT services |
| `ble_svc_bas_init()` | Registers the Battery Service (required by HID profile) |
| `ble_gatts_count_cfg()` / `ble_gatts_add_svcs()` | Registers the custom HID GATT service and its characteristics |
| `ble_gap_adv_set_fields()` / `ble_gap_adv_rsp_set_fields()` | Sets advertising payload (HID UUID, appearance 0x03C1) and scan-response (device name) |
| `ble_gap_adv_start()` | Starts undirected general discoverable advertising |
| `ble_gap_security_initiate()` | Initiates BLE SM pairing from the peripheral side (required for macOS) |
| `ble_svc_gatt_changed()` | Sends a GATT Service Changed indication — activates the macOS HID driver on first secure connection |
| `ble_gatts_notify_custom()` | Sends an 8-byte HID Input Report notification (key press or key release) |
| `ble_store_util_delete_peer()` | Deletes a single peer's bond when encryption fails (stale LTK recovery) |
| `ble_store_clear()` | Wipes all stored bonds from NVS (triggered by long-press reset) |
| `ble_hs_cfg.sm_sc = 1` | Enables LE Secure Connections (LESC) pairing |

### HID Report Format

The device uses a standard **8-byte keyboard report** with no Report ID prefix, compatible with all three target platforms:

```
Byte 0 : Modifier keys (0x02 = Left Shift for uppercase letters)
Byte 1 : Reserved (always 0x00)
Byte 2 : Keycode[0] (HID usage ID, e.g. 0x04 = A … 0x27 = 0)
Byte 3–7 : Keycode[1–5] (always 0x00 — one key at a time)
```

A key press sends the report with the keycode populated; a key release sends all zeros 10 ms later.

---

## Project Structure

```
.
├── main/
│ ├── main.c # App entry, Morse logic, button handling, LVGL UI
│ ├── idf_component.yml # Component dependencies (idf >=5.5.4)
│ ├── ble_hid/
│ │ ├── ble_hid_keyboard.c # BLE HID stack (NimBLE, GATT, SM, advertising)
│ │ └── ble_hid_keyboard.h # Public API
│ ├── lcd_driver/
│ │ ├── lcd_driver.c # ST7735 SPI init via esp_lcd
│ │ └── lcd_driver.h
│ └── lvgl_driver/
│ ├── lvgl_driver.c # LVGL port init and display registration
│ └── lvgl_driver.h
├── CMakeLists.txt
├── partitions.csv
└── sdkconfig
```

---

## 3D Printed Housing

A 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.

![3D print preview](misc/3d_printing.jpeg)

| File | Description |
|---|---|
| [`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. |
| [`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. |

---

## Usage

| Action | Result |
|---|---|
| Short press (< 300 ms) | Add dot `.` to current sequence |
| Long press (≥ 300 ms, < 3 s) | Add dash `-` to current sequence |
| 1 second silence | Decode accumulated sequence → send character via BLE |
| Press & hold ≥ 3 s | Clear sequence, history, and all BLE bonds |

### Pairing

1. Power on — device advertises as `Morse_Keyer`
2. Open Bluetooth settings on host and select `Morse_Keyer`
3. Confirm pairing — blue dot appears on display when secured
4. Open any text field and start typing in Morse

To force a clean re-pair, hold the button for 3 seconds, then forget `Morse_Keyer` on the host before reconnecting.