Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
-e ESP32_DEV38P -e ESP32_DEV38P_nimble \
-e ESP32S3SuperMini -e ESP32S3SuperMini_nimble \
-e RM_pico -e RM_pico_nimble \
-e RM_Wireless -e RM_Wireless_nimble \
-e pico32 -e pico32_nimble

- name: Assemble web flasher
Expand All @@ -69,6 +70,8 @@ jobs:
place ESP32S3SuperMini_nimble esp32s3supermini nimble
place RM_pico rmpico bluedroid
place RM_pico_nimble rmpico nimble
place RM_Wireless rmwireless bluedroid
place RM_Wireless_nimble rmwireless nimble
place pico32 pico32 bluedroid
place pico32_nimble pico32 nimble

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ doc/AUDIT.md

# Web flasher firmware (generated by the Pages workflow)
webflash/firmware/

# For local only
.local
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ and exposes BLE (GATT service `0xFFF0`, characteristic `0xFFF6`).
- Primary target: **ESP32-C3 SuperMini** (`PCB_C3SUPERMINI`). **ESP32-DEV-38P**
(`PCB_WROOM`, environments `ESP32_DEV38P*`) is the second supported board.
- Reference targets (not hardware-validated): the **ESP32-S3 SuperMini**
(`ESP32S3SuperMini*`) and the ESP32 Pico entries (`pico32`, `RM_pico` and their
`_nimble` variants), kept for community testing.
(`ESP32S3SuperMini*`), the ESP32 Pico entries (`pico32`, `RM_pico` and their
`_nimble` variants) and the **RadioMaster Wireless Module** (`RM_Wireless*`),
kept for community testing. The Wireless Module's Bluedroid and NimBLE builds
have been smoke-tested over USB/BLE (AT bench, advertising, GATT, telemetry,
status LED) but not with a radio; see `doc/BENCH.md` before promoting it.
- BLE hosts: **Bluedroid** (default) and **NimBLE** (`USE_NIMBLE`), sharing the
same application code behind `#if defined(USE_NIMBLE)`.
- Reference radio: Radiomaster TX16S MK3 with EdgeTX.
Expand Down Expand Up @@ -111,6 +114,8 @@ pio run -e ESP32_DEV38P # ESP32-DEV-38P, Bluedroid
pio run -e ESP32_DEV38P_nimble # ESP32-DEV-38P, NimBLE
pio run -e ESP32S3SuperMini # ESP32-S3 SuperMini (reference), Bluedroid
pio run -e ESP32S3SuperMini_nimble # ESP32-S3 SuperMini (reference), NimBLE
pio run -e RM_Wireless # RadioMaster Wireless Module (reference), Bluedroid
pio run -e RM_Wireless_nimble # RadioMaster Wireless Module (reference), NimBLE
pio test -e native # host unit tests (requires gcc)
```

Expand Down
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,24 @@ from a supply that can handle the ESP32 current peaks.
| ESP32-S3 SuperMini *(reference)* | UART1 | GPIO43 | GPIO44 | native USB Serial/JTAG |
| ESP32 Pico Kit *(reference)* | UART2 | GPIO4 | GPIO5 | UART0 (default console) |
| Radiomaster Pico *(reference)* | UART0 | GPIO1 | GPIO3 | UART1 — GPIO20 (TX) / GPIO19 (RX) |
| RadioMaster Wireless Module *(reference)* | UART0 | GPIO1 | GPIO3 | USB-C CP2102 (UART0 shared, no log console) |

The pins are selected per environment through the `PCB_*` macro in
`src/board.h`. On the C3 SuperMini the radio uses UART1 on GPIO20/21 (the
default UART0 pins), so the log console is routed to the native USB Serial/JTAG
port instead of UART0; the ESP32-S3 SuperMini uses the same approach with the
default UART0 pins on GPIO43/44. On the ESP32-DEV-38P the on-board CP2102
bridge keeps UART0 free for flashing and logs. On the Radiomaster Pico the radio
link uses UART0, so the log console is moved to UART1.
link uses UART0, so the log console is moved to UART1. On the RadioMaster
Wireless Module a CH442E mux selected by USB VBUS connects UART0 either to the
radio bay or to the CP2102, leaving no pin for a log console: that target is
built silent to keep the AT link clean. Its on-board WS2812 LED (GPIO18) shows
the link state: red = idle, green = radio traffic, blue = BLE peer connected,
cyan = both.

The reference rows are the non-validated targets (see
[Validated targets](#validated-targets)).
The reference rows are the community builds (see
[Validated targets](#validated-targets)); the Wireless Module has only been
validated over USB/BLE so far, not against a radio.

## EdgeTX protocol

Expand Down Expand Up @@ -123,7 +130,8 @@ NimBLE.

**Reference targets** (not hardware-validated). The ESP32-S3 SuperMini uses the
default 1 MB app partition; the Pico boards use 1.31 MB via
`pico_partitions.csv`:
`pico_partitions.csv`; the Wireless Module uses 3 MB via
`rm_wireless_partitions.csv`:

| Environment | BLE host | Flash | Board |
|---|---|---|---|
Expand All @@ -133,12 +141,21 @@ default 1 MB app partition; the Pico boards use 1.31 MB via
| `RM_pico_nimble` | NimBLE | ~41 % | Radiomaster Pico |
| `pico32` | Bluedroid | ~65 % | ESP32 Pico Kit |
| `pico32_nimble` | NimBLE | ~41 % | ESP32 Pico Kit |
| `RM_Wireless` | Bluedroid | ~24 % | RadioMaster Wireless Module |
| `RM_Wireless_nimble` | NimBLE | ~17 % | RadioMaster Wireless Module |

The Wireless Module rows are the same application code, and both builds have
been checked end to end over USB/BLE: advertising, service `0xFFF0` /
characteristic `0xFFF6`, S.Port telemetry decoded by the phone app and the
on-board status LED. The radio bay and the central/headtracker runs are still
untested, so it stays in this table.

```bash
pio run -e C3SuperMini -t upload # C3 SuperMini, Bluedroid
pio run -e C3SuperMini_nimble -t upload # C3 SuperMini, NimBLE
pio run -e ESP32S3SuperMini -t upload # ESP32-S3 SuperMini, Bluedroid
pio run -e RM_pico -t upload # Radiomaster Pico, Bluedroid
pio run -e RM_Wireless -t upload # RadioMaster Wireless Module, Bluedroid
```

The Bluedroid path is kept behind `#if !defined(USE_NIMBLE)`, so both hosts
Expand All @@ -154,10 +171,11 @@ through GitHub Pages. Open the Pages URL in a Chromium-based browser

- Boards: **ESP32-C3 SuperMini** (`C3SuperMini*`) and **ESP32-DEV-38P**
(`ESP32_DEV38P*`).
- Reference builds (not hardware-validated): **ESP32-S3 SuperMini**
(`ESP32S3SuperMini*`), **Radiomaster Pico** (`RM_pico*`) and **ESP32 Pico Kit**
(`pico32*`). They differ in the radio UART pins, so if one does not work, try
the other.
- Reference builds: **ESP32-S3 SuperMini** (`ESP32S3SuperMini*`), **Radiomaster
Pico** (`RM_pico*`), **RadioMaster Wireless Module** (`RM_Wireless*`, same
UART0 pins as the Radiomaster Pico; its BLE side is validated over USB, the
radio bay is not) and **ESP32 Pico Kit** (`pico32*`). They differ in the radio
UART pins, so if one does not work, try the other.
- Hosts: **Bluedroid** (default) and **NimBLE**.

Every board is offered with both hosts (each combination is a separate build).
Expand All @@ -180,11 +198,22 @@ Hardware validation status of the current firmware. The bench procedure is in
`n/t` = not tested (the ESP32-DEV-38P was validated against the phone and the
headtracker, without a board-to-board master run).

Partial validation (BLE path over USB, no radio yet):

| Target | BLE host | AT commands | Slave + telemetry | Master | Headtracker |
|---|---|---|---|---|---|
| RadioMaster Wireless Module | Bluedroid | PASS | PASS | n/t | n/t |
| RadioMaster Wireless Module | NimBLE | PASS | PASS | n/t | n/t |

Both hosts pass the full `tools/at_bench.py` (16 checks) and the phone checks
described in `doc/BENCH.md`; the radio bay (`J4`), central mode and the
headtracker are not tested yet.

The other board environments in `platformio.ini` (ESP32 Pico, ESP32-S3
SuperMini, C3 DevKitM-1, Node32s Wroom32, Radiomaster Pico) are kept for
reference and are not hardware-validated with this version. The ESP32-S3
SuperMini and the two Pico boards are additionally built by the web flasher
workflow so they can be tried from the browser.
SuperMini, the two Pico boards and the Wireless Module are additionally built by
the web flasher workflow so they can be tried from the browser.

## Tests

Expand Down
36 changes: 36 additions & 0 deletions boards/rm_wireless.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"build": {
"arduino": {
"ldscript": "esp32_out.ld"
},
"core": "esp32",
"extra_flags": "-DARDUINO_ESP32_DEV",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "esp32",
"partitions": "rm_wireless_partitions.csv"
},
"connectivity": [
"wifi",
"bluetooth"
],
"debug": {
"openocd_board": "esp-wroom-32.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "RadioMaster Wireless Module",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"require_upload_port": true,
"speed": 460800
},
"url": "https://www.radiomasterrc.com/",
"vendor": "RadioMaster"
}
161 changes: 159 additions & 2 deletions doc/BENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ to repeat the same checks for each BLE host (Bluedroid and NimBLE).
- Reference only (not validated): ESP32-S3 SuperMini (`ESP32S3SuperMini`,
`ESP32S3SuperMini_nimble`, radio UART GPIO43 TX / GPIO44 RX, native USB
console), ESP32 Pico Kit (`pico32`, `pico32_nimble`, radio UART GPIO4 TX /
GPIO5 RX) and Radiomaster Pico (`RM_pico`, `RM_pico_nimble`, radio UART
GPIO1 TX / GPIO3 RX, console on UART1 GPIO20/19).
GPIO5 RX) and Radiomaster Pico (`RM_pico`, `RM_pico_nimble`, radio UART GPIO1
TX / GPIO3 RX, console on UART1 GPIO20/19).
- RadioMaster Wireless Module (`RM_Wireless`, `RM_Wireless_nimble`, radio UART0
GPIO1 TX / GPIO3 RX shared with the USB-C CP2102 bridge, no log console).
Partially validated over USB/BLE (see its notes below); the radio bay has not
been tested yet.
- USB-UART adapter (FTDI) on COM10: TX wired to the module RX (UART link
used for AT commands and telemetry injection), 115200 baud.
- Phone with the telemetry app (Android Taranis SmartPort app, RomanLut fork).
Expand All @@ -38,6 +42,8 @@ pio run -e ESP32_DEV38P -t upload # ESP32-DEV-38P, Bluedroid
pio run -e ESP32_DEV38P_nimble -t upload # ESP32-DEV-38P, NimBLE
pio run -e ESP32S3SuperMini -t upload # ESP32-S3 SuperMini, Bluedroid
pio run -e ESP32S3SuperMini_nimble -t upload # ESP32-S3 SuperMini, NimBLE
pio run -e RM_Wireless -t upload # RadioMaster Wireless Module, Bluedroid
pio run -e RM_Wireless_nimble -t upload # RadioMaster Wireless Module, NimBLE
```

If the board does not reset cleanly, power-cycle it and use the manual
Expand Down Expand Up @@ -193,3 +199,154 @@ Last run (A = NimBLE, B = Bluedroid, headtracker at -84 dBm):
no `OK+DISCE`).
- Telemetry app: connection can drop right after connecting when the
S.Port frames are invalid (fixed by the checksum in `tools/sport_sender.py`).

## RadioMaster Wireless Module notes

Reference target (`RM_Wireless`, `RM_Wireless_nimble`). Verified on hardware:
ESP32-D0WDQ6-V3, W25Q64 (8 MB), 40 MHz crystal, CP2102N bridge, flash erased
from the factory, 3 MB application partition.

The board shares UART0 (GPIO1 TX / GPIO3 RX) between the radio bay (J4) and the
CP2102N through a CH442E mux selected by USB VBUS. The build therefore runs with
the log console disabled (`CONFIG_LOG_DEFAULT_LEVEL_NONE`): with the USB
unplugged the bay carries the AT protocol, so any log output would corrupt it.
**A silent serial monitor is the expected result, not a fault.** Only the ROM
banner (`ets Jul 29 2019 ...`, `entry 0x40080578`) is visible, and only after a
reset.

### Talking AT over USB

The CP2102N drives `EN_ESP32` and `BOOT` through Q1/Q2 (S8050), so the modem
control lines matter:

- Open the port with **DTR and RTS deasserted**. Most Python tooling (pyserial,
and therefore `pio device monitor`) asserts both at open, which resets the
board and can leave it in the ROM download mode.
- If `GPIO0` was low when the chip booted, it stays in download mode and never
starts the application, so every AT command times out. Pulse `EN` with `GPIO0`
already high (assert RTS, release, with DTR low) and send the command on the
same open handle. The banner must end in `entry 0x40080578`, not in
`waiting for download`.
- `esptool` is unaffected: its reset sequence enters download mode on purpose.

Verified PowerShell snippet (no reset, no dependencies):

```powershell
$port = New-Object System.IO.Ports.SerialPort '<COMx>',115200,'None',8,'One'
$port.DtrEnable = $false
$port.RtsEnable = $false
$port.Open()
$port.Write("AT+ROLE0`r`n")
Start-Sleep -Milliseconds 700
$port.ReadExisting()
$port.Close()
```

### The USB link is picky

On the reference Windows host the CP2102N only enumerates on some ports with some
cables. Symptoms seen during bring-up:

- `Dispositivo USB desconocido (Error de solicitud de descriptor de dispositivo)`,
shown as `USB\VID_0000&PID_0002` (Code 43): the device never completes USB
enumeration.
- The COM port drops off the bus while the ESP32 keeps running; reads fail with
`ClearCommError failed (PermissionError(13, 'Acceso denegado'))`, and the port
only comes back after unplugging and replugging.
- Drops are intermittent, and a reset makes them more likely (the ESP32 current
transient at boot, when the RF calibration runs, is the worst case).

Both are physical-layer problems, not firmware: the ESP32 never panics and the
ROM banner plus `entry 0x40080578` are printed normally every time. Switching to
another USB port, direct on the motherboard, fixed it on the bench. Do not chase
the firmware when the COM port disappears at USB level.

### Running `tools/sport_sender.py` here

On this board the only path to the ESP32 is the CP2102N, so the script has to
drive that COM port: while USB is plugged the CH442E routes UART0 to the bridge,
not to the bay. `serial.Serial(...)` asserts DTR and RTS in `open()`, which
resets the board and can leave it in download mode, so clear the modem lines
first and reuse the module's own builders:

```python
import sys
import time

import serial

sys.path.insert(0, 'tools')
from sport_sender import SENSORS, sport_frame

s = serial.Serial()
s.port, s.baudrate = 'COMx', 115200
s._dtr_state = s._rts_state = False # keep DTR/RTS low when the port opens
s.open()
while True:
for data_id, value in SENSORS:
s.write(sport_frame(0x1B, data_id, value))
time.sleep(0.03)
```

The same frames decode in the phone app as RSSI 95, altitude 123.45 m,
vertical speed -2.50 m/s, voltage 11.40 V, current 15.20 A, RPM 9800,
temperature 23.5 C, GPS altitude 120 m and GPS speed 45 kn (shown as 83 km/h).

### Status LED

The board has a WS2812-compatible RGB LED (`LED1`, TX1812MJA5-1515) on
**GPIO18**, fed through R15 (300R) on the `RGB_ESP32` net. `src/status_led.c`
drives it with the RMT peripheral (GRB, MSB first) when `HAS_STATUS_LED` is
defined, which only happens for `PCB_RM_WIRELESS`; on the other boards the
functions are no-ops.

| Colour | Meaning |
|---|---|
| Red | Powered, no radio traffic and no BLE peer |
| Green | Radio UART active (a byte arrived in the last 10 s) |
| Blue | BLE peer connected (either role) |
| Cyan | Both at once |

"Radio active" is inferred from UART RX activity because the bay has no
presence pin: telemetry streams continuously, and an AT burst refreshes the
window. During a USB-only bench your own commands and injected frames also
count as radio traffic, so green/cyan is expected there.

### Validation status

Checked on hardware with both BLE hosts:

| Check | Bluedroid | NimBLE |
|---|---|---|
| Chip / flash / crystal (ESP32-D0WDQ6-V3, 8 MB W25Q64, 40 MHz) | PASS | PASS |
| Boot + advertising (`BTWifiMod`, MAC `B4:8A:0A:BE:E6:3A`) | PASS | PASS |
| AT bench (`tools/at_bench.py`, 16 checks) | PASS | PASS |
| GATT `0xFFF0` primary / `0xFFF6` READ+WRITE+NOTIFY / CCCD `0x2902` | PASS | PASS |
| GATT value `11-22-33` read from the phone | PASS | PASS |
| Connection held with no `DisConnected` | PASS (90 s) | PASS |
| UART -> notification path (S.Port frames) | PASS (657) | PASS (657) |
| Telemetry decode in the phone app (nine sensors, right units) | PASS | PASS |
| Status LED: red / green / blue / cyan | PASS | PASS |

The BT MAC is derived from the WiFi MAC (`b4:8a:0a:be:e6:38` ->
`b4:8a:0a:be:e6:3a`).

Note that the `Connected:<addr>` line repeats every 2 s while a peripheral is
connected (`runBTPeripherial()` in `terminal.c`): that is the firmware keep-alive
for EdgeTX, not a reconnect loop. A real disconnect emits `DisConnected` plus
two `ERROR` lines.

Still pending, so the target stays a reference build and is not part of
`build-firmware.yml`:

- central mode against a real peripheral and the headtracker test,
- the radio bay (J4) with EdgeTX, which is the only path that validates the
CH442E split and the real protocol handshake.

Two bench caveats found on this board:

- Run the AT bench with the phone disconnected: while a peripheral is
connected, the 2 s `Connected:` keep-alive lands inside the "no response"
windows of `AT+DISC?` and `AT+CLEAR` and makes those checks fail.
- `tools/at_bench.py` sends `AT+NAME`, which persists in NVS. Restore the name
afterwards (`AT+NAMEBTWifiMod`) if the board is going to be handed over.
Loading
Loading