Skip to content

Add authenticated audio streaming and BLE throughput measurement - #75

Merged
MacNite merged 13 commits into
mainfrom
claude/blissful-turing-8edvzo
Sep 9, 2026
Merged

Add authenticated audio streaming and BLE throughput measurement#75
MacNite merged 13 commits into
mainfrom
claude/blissful-turing-8edvzo

Conversation

@MacNite

@MacNite MacNite commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds authenticated, on-request audio streaming over BLE to HiveInside, along with a temporary throughput measurement spike to validate the link capacity for audio delivery. The audio feature enables the node to capture PCM16 samples from the microphone and stream them to HiveHub via GATT notifications, with HMAC-SHA256 authentication and configurable gain control.

Key Changes

Audio Streaming Feature

  • New src/audio.c: Complete authenticated audio service with:

    • GATT service exposing CTRL (write), DATA (notify), and STATUS (read/notify) characteristics
    • HMAC-SHA256 authentication using a provisioned 32-byte key
    • PCM16 capture at 16 kHz with configurable gain (-20 to +20 dB)
    • 32 KiB ring buffer to absorb notification jitter without buffering entire clips
    • Dual-thread architecture: capture thread reads from microphone, TX thread sends notifications
    • Session state machine (IDLE → ARMED → STREAMING → DONE) with error reporting
    • CRC-32 checksum of transmitted audio for integrity verification
    • Clipping detection and reporting as percentage of samples
  • New src/audio.h: Public interface for audio initialization and link lifecycle callbacks

  • New src/audio_secret.example.h: Template for provisioning the authentication key (gitignored in actual deployment)

Connection Management

  • New src/link.c and src/link.h: Centralized ownership model for the single peripheral BLE connection
    • Prevents multiple services from competing for the connection
    • Implements claim/release semantics with timeout-based arm window
    • Tracks connection parameters and PHY updates
    • Shared by OTA, audio, and throughput services

Microphone API Refactoring

  • Modified src/mic.c and src/mic.h: Extracted streaming functions for audio use:
    • mic_stream_start(): Initialize and start PDM capture
    • mic_stream_read(): Non-blocking read from capture buffer
    • mic_stream_release(): Release buffer back to slab
    • mic_stream_stop(): Stop PDM capture
    • Increased block count from 4 to 6 to support concurrent audio streaming

OTA Service Updates

  • Modified src/ota.c and src/ota.h: Refactored to use shared link management
    • Removed local connection tracking and arm timeout logic
    • Added ota_link_connected() and ota_link_disconnected() callbacks
    • Now uses link_claim() and link_release() for connection ownership

Temporary Throughput Measurement Spike

  • New src/throughput.c and src/throughput.h: Diagnostic service to measure BLE notification throughput

    • Blasts configurable payload sizes for a specified duration
    • Tracks sequence numbers to distinguish packet loss from slowness
    • Allows connection interval adjustment from client without reflash
    • Compiles to nothing unless ENABLE_THROUGHPUT_SPIKE=1
  • New tools/throughput/ble_throughput.py: Python client (bleak) for laptop-based throughput testing

  • New throughput-spike.conf: Kconfig fragment to enable measurement build

Configuration and Documentation

  • Modified src/hive_config.h: Added audio configuration parameters:
    • ENABLE_AUDIO: Feature flag (defaults to 1, fails closed without key)
    • HIVE_AUDIO_MAX_SECONDS: Session timeout (60 seconds)
    • HIVE_AUDIO_RING_BYTES: Ring buffer size (32 KiB)
    • HIVE_AUDIO_STALL_TIMEOUT_MS: Notification stall detection (5 seconds)
    • HIVE_AUDIO_CONN_INTERVAL_UNITS: Optimized 15 ms interval (12 units)
    • HIVE_LINK_ARM_TIMEOUT_MS: Connection arm window

https://claude.ai/code/session_01ExXyz8C48CXmsDwyGZP4yf

MacNite and others added 13 commits September 7, 2026 06:53
Issue #71 asks for an on-request audio recording, which means moving a clip
from the node to the relay as GATT notifications out of RAM. Nothing in this
firmware does that, so nothing measures it. The only figure available is the
OTA relay's, measured on a deployment at ~1-1.5 kB/s -- and that path is a poor
proxy in three ways that all push the same direction: it is central to
peripheral, every chunk is a write WITH RESPONSE (two connection intervals
minimum), and data_write() flushes RRAM synchronously inside the ATT callback,
where each 512-byte write runs in a radio timeslot that pre-empts connection
events. It largely measures flash scheduling, not the radio.

So measure the radio directly, before the design commits to streaming a clip or
to buffering a whole one:

* src/throughput.c/.h -- a GATT service (0x8e8b00fx, clear of any future audio
  service) that notifies a sequence-numbered counter pattern for a requested
  duration and reports what it managed to send. The client compares its own
  byte count against the node's, so packet loss is distinguishable from
  slowness. It also logs the negotiated interval, PHY and data length, which is
  where "are we really running 2M PHY and 251-byte PDUs?" is actually answered.
* throughput-spike.conf -- layered like low-power.conf. Carries the Kconfig the
  link-parameter callbacks need and CONFIG_BT_BUF_ACL_TX_COUNT, which
  throughput.c takes its in-flight credit count from, so notifications per
  connection event can be swept without touching code. The connection interval
  is swept from the client through an optional CTRL byte.
* tools/throughput/ble_throughput.py -- laptop client (bleak). An upper bound,
  useful for telling "the node is the limit" from "the relay is the limit".
* docs/ble-throughput-spike.md -- how to run it, how to read it, how to remove
  it.

ENABLE_THROUGHPUT_SPIKE defaults to 0, so a build that does not apply the
fragment is byte-identical with these files present. ota_release_arm_timeout()
is the one hook into existing code: ota.c drops any central that has not sent
BEGIN within six seconds, which would cut every run short. It is compiled out
of normal builds, leaving that guard unconditional in anything that ships.

A spike image must never be released -- the service lets anything in radio
range make the node transmit continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExXyz8C48CXmsDwyGZP4yf
bt_conn_le_info.interval (1.25 ms units) is deprecated -- it cannot represent
the shorter intervals CONFIG_BT_SHORTER_CONNECTION_INTERVALS allows, so it now
sits in a union behind the microsecond field. NCS builds deprecation warnings
as errors, so the connect-time log line failed the build on v3.3.1.

interval_us is the replacement and carries strictly more information. latency
and timeout are not deprecated and are unchanged, and the le_param_updated
callback still takes the interval in 1.25 ms units as an argument, so its print
stays as it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExXyz8C48CXmsDwyGZP4yf
Fix throughput-spike OTA timeout build by defining work item before use
…-feature

Add authenticated BLE audio streaming, shared link ownership, mic stream API, and config/docs
…ing-measurement-cycle

Serialize sensor measurements with BLE sessions
"Provision the same random 32-byte key on node and HiveHub" left out the part
that actually costs time: the two sides want the same bytes in different
notations — a C array here, a 64-character hex string in HiveHub's secrets.h —
so anyone following the old wording had to convert by hand and hope.

Give them the one command that prints both forms, and a filled-in example of the
resulting audio_secret.h so there is no doubt about where the rows go.

Also states two things that were only discoverable by hitting them: flashing one
side and not the other leaves audio failing authentication while the beacon and
its measurements carry on normally (so it presents as a radio fault, not a
configuration one), and HiveHub's FORCE_RESEED has nothing to do with this key —
that flag re-seeds a claim code into NVS, while the audio key is compiled in and
read afresh at every session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExXyz8C48CXmsDwyGZP4yf
Three gaps between the docs and the firmware on this branch.

**The 0.6.2 fix was undocumented.** `audio-over-ble.md` said only that
"periodic sensing pauses for any connection" — which was the intent, not the
mechanism, and was untrue before 0.6.2. A measurement cycle that had already
passed the `link_is_busy()` check went on to drop the sensor rail underneath a
running session, and the boolean rail flag made the session's own enable a
no-op, so a full-length recording carried sound for ~50 ms and the ±4 LSB of an
unpowered microphone thereafter. Every counter reported it clean, because the
bytes that arrived were exactly the bytes that were sent. That failure mode is
worth writing down: it looks like nothing, and only listening reveals it. The
new section describes the session gate, the reference-counted rail, and the move
of session setup out of the GATT callback into the capture thread (which is why
STREAMING now means the microphone is genuinely running). Two troubleshooting
entries follow from it — busy `12`, and audio that starts and then goes silent.

**Key provisioning was missing here.** The section landed on the feature branch
after it was merged, so this branch still told the reader to "provision the same
key" without the command that prints both notations. Cherry-picked.

**The throughput spike still read as an open question.** It is answered —
137 kB/s, which is what decided raw PCM streamed rather than a codec — so the
page now leads with the answer and says plainly that the spike is safe to
delete, rather than leaving a "delete when answered" note that has quietly been
satisfied for a while.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExXyz8C48CXmsDwyGZP4yf
@MacNite
MacNite merged commit 9f60d67 into main Sep 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants