Skip to content
Draft
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
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ Key invariants:
CRSF/Ghost are forwarded raw. The module is a pure passthrough.
- Master trainer = central (`AT+ROLE1`), slave/telemetry = peripheral
(`AT+ROLE0`).
- The firmware stays at 115200 baud; `AT+BAUD4` is acknowledged implicitly by
EdgeTX switching to 115200 without checking a response.
- UART starts at 115200 for the EdgeTX boot handshake. `AT+BAUD4` selects
115200; `AT+BAUD6` selects 460800 for mono 16 kHz A2DP PCM. Both baud changes
have no response, so EdgeTX must switch its UART immediately after sending
the command.

## Build & test

Expand Down
97 changes: 79 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Documentation: https://btwifimod.gitbook.io/
| Auto-connect to the last known device | Supported |
| Device name from `AT+NAME` (persisted and advertised) | Supported |
| Selectable BLE host: Bluedroid or NimBLE | Supported |
| Classic A2DP audio source (Bluedroid, Classic-BT boards) | Supported |

## How telemetry works

Expand Down Expand Up @@ -49,8 +50,9 @@ service (`0xFFF0`, characteristic `0xFFF6`, NOTIFY).

## Wiring

The module talks to the radio over a 3.3 V UART link (8N1, 115200 baud). Only
TX, RX and GND are required; add +5V if the module is powered from the radio.
The module talks to the radio over a 3.3 V UART link (8N1, 115200 baud at
startup). A2DP PCM mode can switch the link to 460800 baud. Only TX, RX and GND
are required; add +5V if the module is powered from the radio.
The data lines are **crossed**: the module TX goes to the radio RX and the
module RX to the radio TX. Keep GND common with the radio, and power the module
from a supply that can handle the ESP32 current peaks.
Expand All @@ -74,12 +76,16 @@ The last two rows are the reference, non-validated targets (see

## EdgeTX protocol

The module speaks the FrSky Bluetooth AT protocol over an 8N1 UART link at
115200 baud. All responses are `\r\n`-terminated; the real ACK that advances
EdgeTX is the `Central:<addr>` / `Peripheral:<addr>` line emitted after
`AT+TXPW` / `AT+ROLE`. Trainer and S.Port frames are `0x7E`-delimited and
byte-stuffed, while CRSF/Ghost are forwarded raw. See `AGENTS.md` for the full
set of protocol invariants.
The module speaks the FrSky Bluetooth AT protocol over an 8N1 UART link. The
link starts at 115200 baud for the boot handshake. `AT+BAUD4` selects 115200;
`AT+BAUD6` selects 460800 for mono A2DP PCM. Baud commands have no response, so
EdgeTX must switch its UART immediately after sending the command. A2DP audio
input is raw signed 16-bit little-endian mono PCM at 16 kHz; the module
resamples it for the Bluetooth sink. All AT responses are `\r\n`-terminated;
the real ACK that advances EdgeTX is the `Central:<addr>` / `Peripheral:<addr>`
line emitted after `AT+TXPW` / `AT+ROLE`. Trainer and S.Port frames are
`0x7E`-delimited and byte-stuffed, while CRSF/Ghost are forwarded raw. See
`AGENTS.md` for the full set of protocol invariants.

## Code structure

Expand Down Expand Up @@ -109,14 +115,15 @@ doc/BENCH.md Bench setup, procedure and validation status.
PlatformIO (ESP-IDF). Two BLE hosts are selectable: Bluedroid (default) or
NimBLE.

**Supported targets** (app partition 1 MB):
**Supported targets** (Bluedroid builds for Classic-Bluetooth boards also carry
the A2DP source and use the 1.34 MB `pico_partitions.csv` app partition):

| Environment | BLE host | Flash | Board |
|---|---|---|---|
| `C3SuperMini` | Bluedroid | ~84 % | ESP32-C3 SuperMini |
| `C3SuperMini_nimble` | NimBLE | ~52 % | ESP32-C3 SuperMini |
| `ESP32_DEV38P` | Bluedroid | ~86 % | ESP32-DEV-38P |
| `ESP32_DEV38P_nimble` | NimBLE | ~53 % | ESP32-DEV-38P |
| Environment | BLE host | App partition | Flash | Board |
|---|---|---|---|---|
| `C3SuperMini` | Bluedroid | 1 MB | ~84 % | ESP32-C3 SuperMini |
| `C3SuperMini_nimble` | NimBLE | 1 MB | ~52 % | ESP32-C3 SuperMini |
| `ESP32_DEV38P` | Bluedroid + A2DP | 1.34 MB | ~91 % | ESP32-DEV-38P |
| `ESP32_DEV38P_nimble` | NimBLE | 1 MB | ~53 % | ESP32-DEV-38P |

**Reference targets** (app partition 1.31 MB via `pico_partitions.csv`, not
hardware-validated):
Expand Down Expand Up @@ -192,18 +199,72 @@ module:

| Command | Meaning |
|---|---|
| `AT+BAUD4` | baud rate level (this firmware stays at 115200) |
| `AT+BAUD4` | switch to 115200 baud |
| `AT+BAUD6` | switch to 460800 baud for mono A2DP PCM |
| `AT+NAME<name>` | set the Bluetooth name (persisted and advertised) |
| `AT+TXPW<n>` | TX power (acknowledged) |
| `AT+ROLE0` | Peripheral (telemetry / trainer slave) |
| `AT+ROLE1` | Central (trainer master) |
| `AT+DISC?` | start a scan (central) |
| `AT+DISC?` | scan BLE in Central mode or Classic Bluetooth in A2DP mode |
| `AT+CON<addr>` | connect to a device (central) |
| `AT+A2DPON` | enable the Classic Bluetooth A2DP source |
| `AT+A2DPCON<addr>` | connect to an A2DP sink (12 hex digits, no separators) |
| `AT+A2DPOFF` | stop A2DP and turn Bluetooth off |
| `AT+A2DPVOL<n>` | set AVRCP absolute volume (0..127) |
| `AT+A2DPVOL?` | query AVRCP's 128 logical volume values |
| `AT+A2DPSTART` / `AT+A2DPSTOP` | play or stop the A2DP sink |
| `AT+BLEON` / `AT+BLEOFF` | enable the last BLE role / disable BLE |
| `AT+BTOFF` | turn Bluetooth off |
| `AT+CLEAR` | forget the saved device |
| `AT+HTRESET` | reset the headtracker (BTWifiModule extension) |

Responses: `OK+...`, `Central:<addr>`, `Peripheral:<addr>`, `Connected:<addr>`,
`DisConnected`, `ERROR`.
`DisConnected`, `A2DPVOL:<n>` (unsolicited sink volume update, 0..127), `ERROR`.
AVRCP exposes a logical range of 0..127; it does not report the speaker's
physical volume-detent count.

A2DP is enabled in the Bluedroid configuration of every Classic-Bluetooth board
(Radiomaster Pico and ESP32-DEV-38P) and never in the NimBLE build, which is
BLE-only. On the ESP32-DEV-38P the AT link is UART2 (GPIO18 TX / GPIO19 RX) while
logs stay on the CP2102 port (UART0), so A2DP is driven from a USB-UART adapter or
the radio, not the log port.
For a test without EdgeTX PCM input, build and flash `RM_pico_a2dp_test`; it
streams a local 500 Hz tone. Connect to a discoverable sink with:

```text
AT+A2DPON
AT+DISC?
AT+A2DPCON<12-digit-address>
```

After the connection is established, send `AT+BAUD6` before transmitting PCM.
To return to the normal radio link speed, send `AT+BAUD4` at 460800 baud.
For PC-fed audio, install `pyserial` on Windows:

```powershell
py -m pip install pyserial
```

Prepare a mono, 16-bit PCM WAV at 16 kHz. If needed, convert an audio file with
FFmpeg:

```powershell
ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le audio.wav
```

Flash the normal `RM_pico` firmware (not `RM_pico_a2dp_test`, which uses its
internal tone), close PuTTY, then run the sender. It probes at 115200 and
460800, resets and restarts the A2DP stack, and restores 115200 if it detects a
460800 session. It defaults to COM4 and the sink MAC `FC58FAACAA1A`:

```powershell
py tools\uart_audio_sender.py audio.wav
```

Override either default with `--port` or `--address`. The sender switches the
module and PC to 460800, verifies the new rate, and prebuffers audio before
connecting. It returns both sides to 115200 when playback ends. If neither baud
probe receives a response, reset the module and retry.

## Credits

Expand Down
34 changes: 32 additions & 2 deletions doc/BENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ to repeat the same checks for each BLE host (Bluedroid and NimBLE).
- ESP32-DEV-38P (ESP32 classic, `ESP32_DEV38P*`): radio UART on
GPIO18 (TX) / GPIO19 (RX); logs on UART0 (its CP2102 bridge). Its auto-reset
does not drive IO0, so enter download mode manually (hold BOOT, tap RESET)
before flashing.
before flashing. The Bluedroid build (`ESP32_DEV38P`) enables Bluetooth
Classic, so this single firmware provides both the BLE roles and the Classic
A2DP/AVRCP audio source; it uses the 1.34 MB `pico_partitions.csv` app
partition because the A2DP stack does not fit in the default 1 MB one.
- Reference only (not validated): 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
Expand Down Expand Up @@ -68,7 +71,8 @@ response. Run it once per firmware variant.
| `AT+TXPW<n>` | `OK+Txpw:0` + mode line | - |
| `AT+DISC?` | `OK+DISCS`, `OK+DISC:<addr>` per peer, `OK+DISCE` | central; peripheral none |
| `AT+CLEAR` | `OK+CLEAR` | central; peripheral none |
| `AT+BAUD<n>` | no response (stays at 115200) | - |
| `AT+BAUD4` | no response; switches to 115200 | - |
| `AT+BAUD6` | no response; switches to 460800 | A2DP PCM |
| `AT+HTRESET` | `OK+HTRESET`, or `ERROR` unless headtracker board | - |
| unknown | no response | - |

Expand Down Expand Up @@ -116,6 +120,24 @@ Notes:
headtracker reads about -80 dBm on the module and -56 dBm on the phone.
Range is therefore limited by the module antenna.

## Classic A2DP audio source (Bluedroid, Classic-BT boards)

Available on `ESP32_DEV38P` (Classic Bluetooth) and `RM_pico`; never on NimBLE.

1. Drive the AT link (for the ESP32-DEV-38P a USB-UART adapter on GPIO18/19, not
the CP2102 log port) and send the sequence
`AT+A2DPON`, `AT+DISC?`, `AT+A2DPCON<12-digit-address>`.
2. Expect `OK+A2DPON`, then `OK+DISCS` / `OK+DISC:<addr>` per sink / `OK+DISCE`
from the inquiry, and finally a started media stream.
3. Volume: `AT+A2DPVOL<n>` (0..127), `AT+A2DPVOL?` to query, and unsolicited
`A2DPVOL:<n>` lines when the sink changes the absolute volume.
4. PCM path: send `AT+BAUD6` after connecting and feed mono 16-bit LE PCM at
16 kHz over the AT link (or use `tools/uart_audio_sender.py`).
5. `AT+A2DPSTOP` / `AT+A2DPSTART` control playback; `AT+A2DPOFF` stops A2DP and
turns Bluetooth off.

BLE and A2DP cannot run at the same time: enabling one stops the other.

## Expected values (telemetry)

| Sensor | Data ID | Sent value | Displayed |
Expand Down Expand Up @@ -154,6 +176,7 @@ headtracker (no board-to-board master run):
| AT command bench (16 checks) | PASS | PASS |
| Slave: telemetry to the phone app | PASS | PASS |
| Headtracker: detect + channels + reset | PASS | PASS |
| Classic A2DP source + AVRCP volume | PASS | n/a |

## Automated battery

Expand Down Expand Up @@ -188,5 +211,12 @@ Last run (A = NimBLE, B = Bluedroid, headtracker at -84 dBm):
- `AT+DISC?` now cancels any in-progress scan first, so it also completes
while the module is auto-connecting (previously it returned `OK+DISCS` with
no `OK+DISCE`).
- `tools/uart_audio_sender.py` waits for an `A2DP media started` line after
`AT+A2DPCON`. The firmware only emits that string from the `AT+A2DPON` handler
and as an `A2DP` log line; on the ESP32-DEV-38P the logs go to the CP2102 port
(UART0), not the AT link (UART2), so the wait can time out even though media is
streaming. The audio path itself is fine: streaming owned PCM at 460800 after
`AT+BAUD6` plays correctly. The handshake is only reliable on the Radiomaster
Pico, where the console and the AT link share UART0.
- Telemetry app: connection can drop right after connecting when the
S.Port frames are invalid (fixed by the checksum in `tools/sport_sender.py`).
14 changes: 13 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

[platformio]
; Primary development target.
default_envs = C3SuperMini
default_envs = RM_pico

; Common configuration for all ESP32 targets (ESP-IDF).
; espressif32@6.10.0 == ESP-IDF v5.4.0 (matches the repo's sdkconfig.* files).
Expand Down Expand Up @@ -98,6 +98,14 @@ build_flags =
-DCPU_ESP32=y
-DBLUETOOTH=y

; A2DP test build: stream a local tone without radio PCM input.
[env:RM_pico_a2dp_test]
extends = env:RM_pico
board_build.esp-idf.sdkconfig_path = sdkconfig.RM_pico
build_flags =
${env:RM_pico.build_flags}
-DA2DP_TEST_TONE

; Same board with the NimBLE host instead of Bluedroid.
[env:RM_pico_nimble]
extends = esp32
Expand Down Expand Up @@ -163,10 +171,14 @@ build_flags =
; ---------------------------------------------------------------------------
; ESP32-DEV-38P (ESP32 DevKit, 38 pins) — Bluedroid
; Radio UART on GPIO18 (TX) / GPIO19 (RX); logs on UART0.
; Classic Bluetooth is enabled in sdkconfig.ESP32_DEV38P, so this single build
; offers both the BLE roles and the Classic A2DP/AVRCP audio source. The A2DP
; stack does not fit in the default 1 MB app partition, hence pico_partitions.csv.
; ---------------------------------------------------------------------------
[env:ESP32_DEV38P]
extends = esp32
board = esp32_dev_38p
board_build.partitions = pico_partitions.csv
upload_protocol = esptool
build_type = release
monitor_speed = 115200
Expand Down
61 changes: 49 additions & 12 deletions sdkconfig.ESP32_DEV38P
Original file line number Diff line number Diff line change
Expand Up @@ -371,14 +371,14 @@ CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set
CONFIG_ESPTOOLPY_FLASHFREQ="40m"
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set
CONFIG_ESPTOOLPY_BEFORE_RESET=y
# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set
Expand Down Expand Up @@ -474,7 +474,25 @@ CONFIG_BT_BLUEDROID_PINNED_TO_CORE=0
CONFIG_BT_BTU_TASK_STACK_SIZE=4096
# CONFIG_BT_BLUEDROID_MEM_DEBUG is not set
CONFIG_BT_BLUEDROID_ESP_COEX_VSC=y
# CONFIG_BT_CLASSIC_ENABLED is not set
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_ENC_KEY_SIZE_CTRL_VSC=y
# CONFIG_BT_ENC_KEY_SIZE_CTRL_NONE is not set
# CONFIG_BT_CLASSIC_BQB_ENABLED is not set
CONFIG_BT_A2DP_ENABLE=y
CONFIG_BT_AVRCP_ENABLED=y

#
# AVRCP Features
#
CONFIG_BT_AVRCP_CT_COVER_ART_ENABLED=y
# end of AVRCP Features

# CONFIG_BT_SPP_ENABLED is not set
# CONFIG_BT_L2CAP_ENABLED is not set
# CONFIG_BT_SDP_COMMON_ENABLED is not set
# CONFIG_BT_HFP_ENABLE is not set
# CONFIG_BT_HID_ENABLED is not set
CONFIG_BT_GOEPC_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_GATTS_ENABLE=y
# CONFIG_BT_GATTS_PPCP_CHAR_GAP is not set
Expand Down Expand Up @@ -690,16 +708,31 @@ CONFIG_BT_BLE_RPA_TIMEOUT=900
#
# Controller Options
#
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
# CONFIG_BTDM_CTRL_MODE_BLE_ONLY is not set
# CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY is not set
# CONFIG_BTDM_CTRL_MODE_BTDM is not set
CONFIG_BTDM_CTRL_MODE_BTDM=y
CONFIG_BTDM_CTRL_BLE_MAX_CONN=2
CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=0
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN=2
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN=0
# CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_HCI is not set
CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_PCM=y
CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=1
CONFIG_BTDM_CTRL_PCM_ROLE_EDGE_CONFIG=y
CONFIG_BTDM_CTRL_PCM_ROLE_MASTER=y
# CONFIG_BTDM_CTRL_PCM_ROLE_SLAVE is not set
CONFIG_BTDM_CTRL_PCM_POLAR_FALLING_EDGE=y
# CONFIG_BTDM_CTRL_PCM_POLAR_RISING_EDGE is not set
CONFIG_BTDM_CTRL_PCM_FSYNCSHP_STEREO_MODE=y
# CONFIG_BTDM_CTRL_PCM_FSYNCSHP_MONO_MODE_LF is not set
# CONFIG_BTDM_CTRL_PCM_FSYNCSHP_MONO_MODE_FF is not set
CONFIG_BTDM_CTRL_PCM_ROLE_EFF=0
CONFIG_BTDM_CTRL_PCM_POLAR_EFF=0
CONFIG_BTDM_CTRL_PCM_FSYNCSHP_EFF=0
# CONFIG_BTDM_CTRL_AUTO_LATENCY is not set
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT=y
CONFIG_BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT_EFF=y
CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF=2
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=0
CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF=2
CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF=0
CONFIG_BTDM_CTRL_PINNED_TO_CORE_0=y
# CONFIG_BTDM_CTRL_PINNED_TO_CORE_1 is not set
Expand Down Expand Up @@ -2203,7 +2236,9 @@ CONFIG_BLUEDROID_PINNED_TO_CORE_0=y
CONFIG_BLUEDROID_PINNED_TO_CORE=0
CONFIG_BTU_TASK_STACK_SIZE=4096
# CONFIG_BLUEDROID_MEM_DEBUG is not set
# CONFIG_CLASSIC_BT_ENABLED is not set
CONFIG_CLASSIC_BT_ENABLED=y
CONFIG_A2DP_ENABLE=y
# CONFIG_HFP_ENABLE is not set
CONFIG_GATTS_ENABLE=y
# CONFIG_GATTS_SEND_SERVICE_CHANGE_MANUAL is not set
CONFIG_GATTS_SEND_SERVICE_CHANGE_AUTO=y
Expand Down Expand Up @@ -2377,12 +2412,14 @@ CONFIG_BLUFI_INITIAL_TRACE_LEVEL=2
CONFIG_SMP_ENABLE=y
# CONFIG_BLE_ACTIVE_SCAN_REPORT_ADV_SCAN_RSP_INDIVIDUALLY is not set
CONFIG_BLE_ESTABLISH_LINK_CONNECTION_TIMEOUT=30
CONFIG_BTDM_CONTROLLER_MODE_BLE_ONLY=y
# CONFIG_BTDM_CONTROLLER_MODE_BLE_ONLY is not set
# CONFIG_BTDM_CONTROLLER_MODE_BR_EDR_ONLY is not set
# CONFIG_BTDM_CONTROLLER_MODE_BTDM is not set
CONFIG_BTDM_CONTROLLER_MODE_BTDM=y
CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN=2
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN=2
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN=0
CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=2
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=0
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=2
CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0
CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0
CONFIG_BTDM_CONTROLLER_HCI_MODE_VHCI=y
Expand Down
Loading
Loading