Skip to content

Commit 21a5495

Browse files
Merge pull request #4 from RobertDaleSmith/esp32
Starts porting firmware to ESP-IDF based firmware for the Seeed XIAO ESP32-S3 board.
2 parents 1abfeaf + 8f6cf39 commit 21a5495

1,700 files changed

Lines changed: 315568 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
- `app/src/` carries the firmware modules (BLE transport, USB HID, inputs); pair each new driver with a matching header.
5+
- `app/prj.conf` and `CMakeLists.txt` hold Zephyr configuration, while optional board overlays sit beside the sources they affect.
6+
- `docs/` stores build notes and assets; `scripts/`, `tools/`, the `Dockerfile`, and `docker-compose.yml` provide repeatable workflows.
7+
- Dependency trees (`modules/`, `zephyr/`, `nrf/`, `nrfxlib/`) are managed by `west` and should remain untouched except through updates.
8+
9+
## Build, Test, and Development Commands
10+
- `docker-compose run --rm mouthpad-build` rebuilds both supported boards inside a containerized Zephyr environment.
11+
- `docker-compose run --rm mouthpad-dev` drops you into an interactive shell with the toolchain and `west` installed.
12+
- `make build BOARD=xiao_ble` (or `adafruit_feather_nrf52840`) wraps `west build -b <board> app --pristine=always`; results land in `build/app`.
13+
- `make flash`, `make flash-uf2`, and `make monitor` program hardware via J-Link, UF2 mass storage, or RTT logging respectively.
14+
- For lighter iterations, invoke `west build -b xiao_ble app` directly and clean with `rm -rf build` when switching boards.
15+
16+
## Coding Style & Naming Conventions
17+
- Follow Zephyr C style: tabs for indentation, 100-character lines, `UPPER_SNAKE_CASE` macros, and module-prefixed statics such as `ble_transport_*`.
18+
- Keep headers limited to public interfaces, declare file-local helpers as `static`, and minimize cross-module coupling.
19+
- Run `clang-format -i <files>` from the repo root so the Zephyr `.clang-format` is honored before sending patches.
20+
- Log through `LOG_INF`, `LOG_DBG`, and friends with concise, hardware-focused phrasing.
21+
22+
## Testing Guidelines
23+
- After flashing, confirm BLE pairing and USB enumeration on a host; capture RTT or USB CDC logs for regressions.
24+
- Place automated checks under `app/tests` with Zephyr `ztest`; reuse the `test/cmock` utilities for mocking hardware seams.
25+
- Execute `west twister -p native_posix_64 --testsuite-root app/tests` to run suites locally and gate pull requests.
26+
27+
## Commit & Pull Request Guidelines
28+
- Adopt Conventional Commit prefixes (`feat`, `fix`, `chore`, `docs`, etc.) consistent with the current history.
29+
- Keep commits focused, exclude generated artefacts (`build/`, `*.uf2`), and ensure `west.yml` changes are intentional.
30+
- PRs should outline tested hardware, configuration implications, and include logs or screenshots when touching DFU or user-visible flows.
31+
32+
## DFU & Hardware Tips
33+
- Copy `build/app/zephyr/app.uf2` to the board’s storage volume (double-tap reset first) for UF2 updates.
34+
- When using J-Link, verify probe firmware and power stability before `make flash`; erratic RTT output usually signals cabling issues.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,9 @@ This project includes GitHub Actions that automatically build and test your code
188188
2. Create a feature branch
189189
3. Make your changes
190190
4. Test thoroughly using one of the build methods
191-
5. Submit a pull request
191+
5. Review the [Repository Guidelines](AGENTS.md) for coding, testing, and PR expectations
192+
6. Submit a pull request
192193

193194
## 📄 License
194195

195-
This project is licensed under the MIT License - see the LICENSE file for details.
196+
This project is licensed under the MIT License - see the LICENSE file for details.

app/prj.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ CONFIG_USB_CDC_ACM_RINGBUF_SIZE=4096
2424
CONFIG_USB_DEVICE_HID=y # USB HID
2525
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=y
2626
CONFIG_USB_DEVICE_PRODUCT="MouthPad^USB"
27-
CONFIG_USB_DEVICE_MANUFACTURER="Augmental.Tech"
27+
CONFIG_USB_DEVICE_MANUFACTURER="Augmental Tech"
2828
CONFIG_USB_DEVICE_SN="12345678901234567890"
2929
CONFIG_USB_DEVICE_PID=0xEEEE
3030
CONFIG_USB_DEVICE_VID=0x1915

esp32/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
cmake_minimum_required(VERSION 3.16)
2+
3+
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
4+
project(mouthpad_esp32_ble_hid_central)

esp32/Makefile

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# ESP32 BLE HID central prototype Makefile
2+
3+
IDF_PATH ?= $(HOME)/esp-idf
4+
IDF_PY ?= idf.py
5+
PYTHON ?= python3
6+
ESP32_TARGET ?= esp32s3
7+
ESP32_PORT ?=
8+
ESP32_BAUD ?= 921600
9+
10+
# Prefer the ROM USB-Serial/JTAG interface (VID:PID 0x303a:0x1001) when a
11+
# device path is not provided. When pyserial is unavailable or no matching port
12+
# is detected we simply fall back to idf.py's own auto-detection.
13+
ifeq ($(strip $(ESP32_PORT)),)
14+
AUTO_SERIAL_JTAG_PORT := $(shell $(PYTHON) -c "import sys\ntry:\n from serial.tools import list_ports\nexcept Exception:\n sys.exit(0)\nports = list_ports.comports()\nfor p in ports:\n if getattr(p, 'vid', None) == 0x303a and getattr(p, 'pid', None) == 0x1001:\n sys.stdout.write(p.device)\n break\nelse:\n for p in ports:\n desc = ((p.description or p.device) or '').lower()\n if 'usb-serial/jtag' in desc or 'usb_serial_jtag' in desc:\n sys.stdout.write(p.device)\n break\n" 2>/dev/null)
15+
endif
16+
17+
ifeq ($(strip $(ESP32_PORT)),)
18+
ESP32_PORT := $(AUTO_SERIAL_JTAG_PORT)
19+
ifneq ($(strip $(ESP32_PORT)),)
20+
$(info Auto-detected ESP32 USB-Serial/JTAG port: $(ESP32_PORT))
21+
endif
22+
endif
23+
24+
IDF_PORT_ARG := $(if $(strip $(ESP32_PORT)),-p $(ESP32_PORT),)
25+
26+
.PHONY: init set-target build flash monitor flash-monitor clean fullclean
27+
28+
init:
29+
. $(IDF_PATH)/export.sh && $(IDF_PY) --version
30+
31+
set-target:
32+
$(IDF_PY) set-target $(ESP32_TARGET)
33+
34+
build: set-target
35+
$(IDF_PY) build
36+
37+
flash:
38+
$(IDF_PY) $(IDF_PORT_ARG) -b $(ESP32_BAUD) flash
39+
40+
build-flash: build
41+
$(IDF_PY) $(IDF_PORT_ARG) -b $(ESP32_BAUD) flash
42+
43+
monitor:
44+
$(IDF_PY) $(IDF_PORT_ARG) monitor
45+
46+
flash-monitor: build
47+
$(IDF_PY) $(IDF_PORT_ARG) -b $(ESP32_BAUD) flash monitor
48+
49+
clean:
50+
$(IDF_PY) clean
51+
52+
fullclean:
53+
$(IDF_PY) fullclean

esp32/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# ESP32 BLE HID Central Prototype
2+
3+
This directory hosts an ESP-IDF project that scans for BLE HID peripherals (e.g., the MouthPad mouse),
4+
connects to the first device discovered, and logs incoming HID reports with the received signal strength
5+
indicator (RSSI). It is adapted from the official `bluetooth/esp_hid_host` example in ESP-IDF; unnecessary
6+
USB/Bluetooth Classic handling has been trimmed so you can focus on BLE performance testing with the
7+
ESP32-S3 and an external antenna.
8+
9+
## Prerequisites
10+
11+
1. Install ESP-IDF v5.1 or later and export the environment (`. $IDF_PATH/export.sh`).
12+
2. Connect the ESP32-S3 board over USB and note its serial port (e.g., `/dev/cu.usbserial-0001`).
13+
3. Optional: adjust defaults in `sdkconfig.defaults` for scan timing, stack size, or other HID host
14+
parameters mirrored from the upstream example.
15+
16+
## Hardware
17+
18+
- Seeed Studio XIAO ESP32-S3 dev board [[amazon](https://www.amazon.com/gp/product/B0BYSB66S5)]
19+
- External 2.4 GHz antenna with MHF4/IPX-to-SMA pigtail adaptor [[amazon](https://www.amazon.com/gp/product/B07R21LN5P)]
20+
21+
Attach the pigtail to the XIAO’s on-board antenna connector. The built-in single-colour user LED on GPIO 21 provides
22+
status feedback (slow blink while scanning, solid on when connected, quick pulses on HID traffic).
23+
24+
## Build, Flash, and Monitor
25+
26+
```bash
27+
# Activate ESP-IDF with the pinned Python environment
28+
. ./env.sh
29+
30+
# (First time only) fetch TinyUSB device stack component
31+
idf.py add-dependency espressif/esp_tinyusb^1.4.2
32+
33+
# Initialise ESP-IDF for this shell
34+
make init
35+
36+
# Build the firmware (defaults to target esp32s3)
37+
make build
38+
39+
# Flash the device (override ESP32_PORT if the default wildcard does not match)
40+
ESP32_PORT=/dev/cu.usbserial-0001 make flash
41+
42+
# Attach a serial monitor (Ctrl+] to quit)
43+
ESP32_PORT=/dev/cu.usbserial-0001 make monitor
44+
```
45+
46+
`make init` sources `$(IDF_PATH)/export.sh` (default `~/esp-idf`) so subsequent
47+
targets run in the ESP-IDF environment. Override `IDF_PATH` when ESP-IDF lives
48+
elsewhere.
49+
50+
### Status LED
51+
52+
The single user LED on the XIAO ESP32-S3 (GPIO 21) behaves like the nRF build:
53+
- slow blink while scanning for a MouthPad
54+
- solid on when connected, with a quick off/on pulse whenever USB HID traffic is forwarded
55+
56+
### Entering DFU without the BOOT button
57+
58+
Open the TinyUSB CDC console (e.g., `screen /dev/cu.usbmodemXXXX 115200`) and send
59+
`dfu` followed by Enter. Close the terminal so the port is free. The firmware
60+
reboots, hands USB back to the ROM loader, and enumerates as the USB-Serial/JTAG
61+
download port. Run `idf.py flash` (or `make flash`) against that port—because the
62+
flasher no longer tries to reset before flashing, the ROM stays available until
63+
the transfer completes, after which the application restarts normally.
64+
65+
`make flash-monitor` combines flashing and monitoring for quicker cycles. Reports are truncated to 32
66+
bytes for readability; adjust the helper in `main/main.c` if full reports are needed.
67+
68+
## Expected Output
69+
70+
Once powered, the firmware scans continuously. When a BLE HID device is detected, the logs show the peer
71+
address, connection events, and HID reports alongside their RSSI value. Connected reports are forwarded
72+
verbatim to the TinyUSB HID interface using the same descriptor and report IDs as the nRF firmware, so a
73+
host sees identical mouse/consumer-control behaviour. A CDC ACM interface is enumerated in parallel and
74+
serves as the default console (the device now appears as both a HID mouse and USB serial port). Use these
75+
logs to compare signal strength under different antenna setups. If you need richer diagnostics, cross-
76+
reference the upstream `esp_hid_host` README for optional features (battery events, Classic Bluetooth)
77+
that can be re-enabled here as needed.

esp32/dependencies.lock

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
dependencies:
2+
espressif/esp_tinyusb:
3+
component_hash: 6a50305bc61c7a361da8c0833642be824e92dacb0a6001719a832a4e96e471bf
4+
dependencies:
5+
- name: idf
6+
require: private
7+
version: '>=5.0'
8+
- name: espressif/tinyusb
9+
registry_url: https://components.espressif.com
10+
require: public
11+
version: '>=0.14.2'
12+
source:
13+
registry_url: https://components.espressif.com/
14+
type: service
15+
version: 1.7.6~2
16+
espressif/tinyusb:
17+
component_hash: aa65639878f27a44d349044afd9c3fc134a92bd560874fdac1d836019b5c07ca
18+
dependencies:
19+
- name: idf
20+
require: private
21+
version: '>=5.0'
22+
source:
23+
registry_url: https://components.espressif.com
24+
type: service
25+
targets:
26+
- esp32s2
27+
- esp32s3
28+
- esp32p4
29+
version: 0.18.0~4
30+
idf:
31+
source:
32+
type: idf
33+
version: 6.0.0
34+
direct_dependencies:
35+
- espressif/esp_tinyusb
36+
manifest_hash: 61a10bf894cb9a8962af9f2822661e2560f5364adff5dd5520c0e5f2c6ac9a84
37+
target: esp32s3
38+
version: 2.0.0

esp32/env.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
# Helper to activate ESP-IDF with the pinned Python 3.13 environment
3+
export IDF_PYTHON_ENV_PATH="$HOME/.espressif/python_env/idf6.0_py3.13_env"
4+
source "$HOME/esp-idf/export.sh"

esp32/main/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
idf_component_register(SRCS "bootloader_trigger.c" "ble_bas.c" "leds.c" "main.c" "ble_transport.c" "usb_hid.c" "usb_cdc.c"
2+
INCLUDE_DIRS "."
3+
REQUIRES bt esp_hid nvs_flash)

esp32/main/ble_bas.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#include "ble_bas.h"
2+
3+
#include "esp_log.h"
4+
5+
#define TAG "ble_bas"
6+
7+
static bool s_ready;
8+
static uint8_t s_battery_level;
9+
10+
static inline uint8_t clamp_u8(int value)
11+
{
12+
if (value < 0) {
13+
return 0;
14+
}
15+
if (value > 255) {
16+
return 255;
17+
}
18+
return (uint8_t)value;
19+
}
20+
21+
esp_err_t ble_bas_init(void)
22+
{
23+
s_ready = false;
24+
s_battery_level = 0xFF;
25+
ESP_LOGI(TAG, "Battery Service helper initialised");
26+
return ESP_OK;
27+
}
28+
29+
void ble_bas_reset(void)
30+
{
31+
s_ready = false;
32+
s_battery_level = 0xFF;
33+
ESP_LOGI(TAG, "Battery Service reset");
34+
}
35+
36+
void ble_bas_handle_level(uint8_t level)
37+
{
38+
if (level == 0xFF) {
39+
ESP_LOGW(TAG, "Invalid battery level received");
40+
s_ready = false;
41+
s_battery_level = 0xFF;
42+
return;
43+
}
44+
45+
s_ready = true;
46+
s_battery_level = level;
47+
ESP_LOGI(TAG, "Battery level: %u%%", level);
48+
}
49+
50+
bool ble_bas_is_ready(void)
51+
{
52+
return s_ready;
53+
}
54+
55+
uint8_t ble_bas_get_battery_level(void)
56+
{
57+
return s_battery_level;
58+
}
59+
60+
ble_bas_rgb_color_t ble_bas_get_battery_color(ble_bas_color_mode_t mode)
61+
{
62+
ble_bas_rgb_color_t color = {0, 0, 0};
63+
64+
if (!s_ready || s_battery_level == 0xFF || s_battery_level > 100) {
65+
color.green = 255;
66+
return color;
67+
}
68+
69+
if (mode == BLE_BAS_COLOR_MODE_DISCRETE) {
70+
if (s_battery_level >= 50) {
71+
color.green = 255;
72+
} else if (s_battery_level >= 10) {
73+
color.red = 255;
74+
color.green = 255;
75+
} else {
76+
color.red = 255;
77+
}
78+
} else {
79+
if (s_battery_level >= 50) {
80+
color.green = 255;
81+
int red = (255 * (100 - s_battery_level)) / 50;
82+
color.red = clamp_u8(red);
83+
} else {
84+
color.red = 255;
85+
int green = (255 * s_battery_level) / 50;
86+
color.green = clamp_u8(green);
87+
}
88+
}
89+
90+
return color;
91+
}
92+

0 commit comments

Comments
 (0)