Add Nordic UART Service BLE bridge for serial commands - #2745
Open
loratrak wants to merge 2 commits into
Open
Conversation
Turn the ble_api module into a standard Nordic UART Service (NUS) so any
BLE central (nRF Connect, a companion app, ...) can drive Bruce's serial
command parser over BLE.
Changes:
- BLESerialService: standard NUS with separate RX (write) and TX (notify)
characteristics instead of a single custom characteristic; RX bytes are
buffered under a mutex and delivered to the parser as complete lines; TX
output is chunked to the negotiated MTU; fixed vprintf (was passing the
va_list straight to sprintf).
- ble_api: advertise as "Bruce" with a scan response so the 128-bit NUS
UUID stays discoverable alongside the battery service.
- settings: show "BLE API ON/OFF" feedback when toggling, so it is clear
the service actually started.
How to use the BLE API:
1. On the device, enable it in Config > Advanced > "Toggle BLE API"
(off by default). It then advertises as "Bruce".
2. Connect and talk to the NUS service:
- Service: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
- RX (write) 6E400002-...: write a command line, e.g. "info\n".
Every command must end with a newline.
- TX (notify) 6E400003-...: subscribe to receive the parser output.
- Battery: standard 0x180F / 0x2A19 is also exposed.
3. Any command accepted by the USB serial CLI works over BLE
(info, free, uptime, ir, subghz, storage, ...).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The NUS bridge felt slow not because GATT is slow but because of how the bridge emitted data and how the link was set up. This reworks both without changing the RX contract (newline-terminated command lines) or the SerialDevice interface seen by the command handlers. Coalesced TX notifications: - print/println/printf/write used to emit one notification per call and sleep a fixed 5 ms after every chunk. A command that prints 40 lines therefore cost 40 notifications, each waiting for its own connection event. Output is now appended to a mutex-guarded buffer and flushed as full MTU-3 byte notifications; a trailing partial chunk is flushed by flush() or automatically ~12 ms later, piggybacking on the poll the command task already does in available(). flush() is no longer a no-op. - Dropped the fixed 5 ms per-chunk delay. Backpressure now comes from bleNotifyRetry() returning false when the controller queues are full, which is the real signal; the fixed sleep was pure latency. Don't stall the USB serial when nobody is listening: - With no central subscribed to the TX characteristic every notify() failed and burned the whole retry budget, so enabling the BLE API slowed down ALL serial output even with no app connected. An onSubscribe callback on the TX characteristic now tracks subscription state and queueTx() simply drops output when there is no subscriber. Link setup: - Request ATT MTU 517 in setup(); previously we advertised the NimBLE default and capped every notification far below what the peer allowed. The central still picks the final value (iOS settles around 185). - Connection params changed from 6..24 to 12..24 (15..30 ms). iOS rejects the entire request when the minimum interval is under 15 ms, which left us on whatever interval iOS chose on its own. - Prefer LE 2M PHY (roughly 2x raw throughput) on chips that have it, guarded out for the classic ESP32 (BLE 4.2, no 2M radio). The peer keeps 1M if it does not support 2M. - Clear the TX buffer and reset MTU/subscription on disconnect. Command loop: - serialcmds polls one command per pass; a burst queued over BLE used to pay the full 10 ms idle tick between each one. Tick drops to 1 ms while input is still buffered, and still always yields. To benefit, a central must subscribe to the TX characteristic (the firmware now only sends to subscribers) and should write to RX with write-without-response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed Changes
Adds a standard Nordic UART Service (NUS) BLE bridge so any BLE central
(nRF Connect, a companion app, ...) can drive Bruce's existing serial command
parser over BLE, and makes that bridge fast.
The bridge maps the NUS RX/TX characteristics onto the existing
SerialDeviceinterface, so every serial command already supported by Bruce works unchanged
over BLE. It is enabled under Config > Advanced > "Toggle BLE API" (off by
default); the device then advertises as "Bruce".
6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E400002-...: write a newline-terminated command line6E400003-...: subscribe to receive parser output0x180F/0x2A19The second commit addresses latency: output was emitted as one BLE notification
per
print/printlnwith a fixed delay per chunk, so a single command produceddozens of notifications, each waiting for its own connection event. The bridge
now:
MTU-3byte notifications instead of one per call;(
notify()returning false when the controller queues are full);notify failed and burned the retry budget, which slowed down USB serial
too whenever the BLE API was enabled;
previous 7.5 ms minimum was rejected outright by iOS), and prefers LE 2M PHY
on chips that support it (guarded out for the classic ESP32, which is BLE 4.2).
Types of Changes
vprintfpreviously passed ava_liststraight tosprintf; TX nolonger stalls USB serial when no central is subscribed)
No breaking changes. The feature is opt-in and off by default; behaviour with
the BLE API disabled is unchanged.
Verification
advertises as "Bruce".
characteristic
6E400003-..., and write a command such asinfo\nto RX6E400002-...using write without response.USB serial console returns.
completes in ~1 s where the pre-optimisation bridge took ~7 s.
unchanged and full speed.
Built and linked for both ESP32-S3 (
lilygo-t-embed-cc1101) and classic ESP32(
Marauder-Mini).Testing
Not covered by automated tests — the change is on the BLE/serial I/O path, which
has no unit-test harness in the project today. Verified manually on hardware
(see Verification). Happy to add coverage if the maintainers can point to an
existing pattern for exercising the
SerialDevicelayer.Linked Issues
None.
User-Facing Change