Embedded SDK guide¶
Overview¶
The Xylolabs embedded SDK targets all supported MCU platforms. Application code runs the same audio and data processing pipelines on each target; the SDK handles memory management, codec encoding, session lifecycle, and transport differences behind the Platform trait.
All SDK crates are no_std compatible. No dynamic memory allocation occurs in the hot path.
Crate Structure¶
crates/
xylolabs-protocol/ # XMBP wire protocol (no_std, shared with server)
xylolabs-sdk/ # SDK core: client, session, codec, transport (no_std)
xylolabs-hal-rp/ # RP2350 HAL -- LTE-M1 modem via UART
xylolabs-hal-stm32/ # STM32 HAL -- F103/F411/WB55/WBA55, UART modem + BLE
xylolabs-hal-esp/ # ESP32 HAL -- S3/C3, native WiFi
xylolabs-hal-nrf/ # nRF HAL -- 52840 BLE GATT, 9160 LTE modem
sdk/examples/ # 12 Embassy examples (one per MCU variant)
sdk/examples/rp2350-full-hardware/ # Full hardware example: audio + sensors + LTE-M1 + watchdog + OTA
tests/e2e-ingest/ # E2E ingest test: 4ch XAP audio + XMBP sensors → live API
Note: HAL crates (
xylolabs-hal-*) andsdk/examples/are NOT in the main workspace. They require cross-compilation toolchains for their respective targets and cannot be built or tested on the host.
Platform Abstraction (Platform trait)¶
The Platform trait at crates/xylolabs-sdk/src/platform.rs is the hardware abstraction boundary. Each MCU family implements it once in its HAL crate. The SDK core never touches hardware directly.
Required methods:
| Method | Purpose |
|---|---|
tcp_connect(host, port, use_tls) |
Establish (optionally TLS) TCP connection |
tcp_send(data) -> usize |
Send data over TCP, returns bytes sent |
tcp_recv(buf, timeout_ms) -> usize |
Receive data, 0 = timeout |
tcp_close() |
Close TCP connection |
tcp_is_connected() -> bool |
Connection state check |
get_time_us() -> u64 |
Monotonic microsecond clock |
sleep_ms(ms) |
Blocking delay |
watchdog_feed() |
Feed hardware WDT |
Optional methods (with defaults):
| Method | Default | Purpose |
|---|---|---|
get_rssi() -> i8 |
0 | Signal strength (LTE/WiFi) |
flash_write(offset, data) -> bool |
false | Persistent storage write |
flash_read(offset, buf) -> bool |
false | Persistent storage read |
XylolabsClient State Machine¶
XylolabsClient<P: Platform, AUDIO_RING, XMBP_BUF> contains:
- platform: P — hardware abstraction
- config: Config — runtime tuning
- session: SessionManager — IDLE → CONNECTING → ACTIVE → CLOSING → ERROR
- transport: HttpTransport — bare HTTP/1.1 transport
- audio_ring: RingBuffer<AUDIO_RING> — lock-free SPSC buffer
- metadata value/timestamp arrays — fixed-size accumulation buffers
- xmbp_buf: [u8; XMBP_BUF] — packet build buffer
- health: Health — uptime, batches, retries, RSSI
- Const generics
AUDIO_RING(default 32KB) andXMBP_BUF(default 16KB) allow static sizing per target. - Up to 16 metadata streams, 32 samples per stream per batch.
- Audio batch interval: 500ms default. Metadata batch interval: 1000ms default.
- Automatic exponential backoff reconnect (200ms base, 30s max, 10 attempts before extended wait).
- Watchdog timeout (8s default) must always exceed HTTP timeout (5s default).
Default Configuration¶
| Parameter | Default | Range |
|---|---|---|
api_host |
api.xylolabs.com |
-- |
api_port |
443 | -- |
use_tls |
true | -- |
audio_channels |
4 | 1--4 |
audio_sample_rate |
16000 | 8000--96000 |
audio_batch_ms |
500 | >0 |
meta_batch_ms |
1000 | >0 |
max_retries |
5 | -- |
watchdog_timeout_sec |
8 | >0, must exceed http_timeout_ms |
retry_base_ms / retry_max_ms |
200 / 30000 | -- |
http_timeout_ms |
5000 | <watchdog timeout |
health_interval_sec |
300 | -- |
Session Lifecycle¶
Lifecycle:
- IDLE → CONNECTING → ACTIVE → CLOSING
- failures move the session into ERROR
- the session manager attempts automatic recovery from ERROR
Sessions are created via POST /api/v1/ingest/sessions with stream definitions. Each stream has:
- stream_index (u16): matches XMBP stream_id
- name (string, max 32B on embedded)
- value_type (u8): XMBP wire tag
- unit (optional, max 16B)
- sample_rate_hz (f32)
The SessionManager handles reconnect with exponential backoff. After MAX_RECONNECT_ATTEMPTS (10), backoff enters extended wait mode.
Ring Buffer¶
RingBuffer<N> at crates/xylolabs-sdk/src/ring_buffer.rs is a lock-free SPSC (single-producer, single-consumer) ring buffer using atomic indices.
Nmust be a power of 2 (compile-time assertion).- Uses
AtomicUsizewith Release/Acquire memory ordering for multi-core safety. UnsafeCellfor internal buffer under the SPSC invariant.- One byte reserved to distinguish empty from full.
HTTP Transport¶
HttpTransport at crates/xylolabs-sdk/src/transport.rs manually builds HTTP/1.1 requests and parses responses without any external HTTP library dependency.
- TCP keep-alive with automatic reconnection.
- Send-all with retry loop for partial sends.
- Response body buffer: 2048 bytes (fixed).
Audio Codecs¶
XAP -- Xylolabs Audio Protocol (Default)¶
XAP is Xylolabs' proprietary MDCT-based spectral audio codec designed for real-time compression of multi-channel audio on resource-constrained MCUs.
Key characteristics:
| Property | Value |
|---|---|
| Transform | Modified Discrete Cosine Transform (MDCT) |
| Compression ratio | 8:1--10:1 (bitrate-dependent) |
| Bitrate range | 16--320 kbps per channel |
| Typical bitrate | 64--80 kbps per channel |
| Algorithmic delay | 7.5--10 ms (one frame) |
| CPU requirement | ~10 MIPS per channel (with DSP acceleration) |
| RAM per channel | ~8 KB encoder state |
| Sample rates | 8, 16, 24, 32, 48, 96 kHz |
| Channels | 1--4 (encoded independently) |
| Frame durations | 7.5 ms, 10 ms |
Encoder pipeline: PCM input → MDCT → Quantization (adaptive step size) → Coefficient packing (8-bit)
Characteristics:
- High compression ratio (8:1--10:1) — Four channels at 96 kHz fit within ~30--40 KB/s sustained uplink.
- Spectral domain encoding — MDCT preserves frequency content across the full spectrum, suitable for industrial audio monitoring and analysis.
- Bitrate-tunable quality — The adaptive quantization step size trades quality for bandwidth at runtime, from 16 kbps to 320 kbps.
- Low algorithmic delay — One frame (7.5--10 ms) of delay.
- No dynamic allocation — All encoder state fits in a single XapEncoder struct with statically-sized buffers.
- Hardware acceleration — CMSIS-DSP (Cortex-M4F/M33) and ESP32-S3 PIE SIMD optimized MDCT paths available via feature flags.
Performance optimization:
- Precomputed fixed-point cosine lookup table for MDCT at sample rates <= 32 kHz (200 KB table). Must be allocated as static on embedded targets.
- For sample rates > 32 kHz, falls back to runtime cosf() / sinf() computation.
- Feature flags: cmsis-dsp (ARM DSP extensions), esp32-simd (Xtensa PIE SIMD).
Requirement: XAP requires a hardware floating-point unit (FPU). Platforms without an FPU must use IMA-ADPCM.
Feature flag: xap (enabled by default).
Full specification: XAP-SPECIFICATION.md
IMA-ADPCM -- Fallback Codec¶
IMA-ADPCM provides 4:1 compression with pure integer arithmetic. No FPU required. Designed for severely constrained targets.
| Property | Value |
|---|---|
| Compression ratio | 4:1 |
| CPU requirement | < 1 MIPS per channel |
| RAM per channel | 3 bytes (predicted + step_index) |
| Bit depth | 4-bit nibbles per sample |
- Uses standard IMA step/index tables (89-entry step table, 16-entry index adjustment).
- Per-channel encoder state:
predicted(i16) +step_index(i8). - No spectral analysis -- time-domain only.
Feature flag: adpcm.
Codec Selection Guide¶
| Target | FPU | Recommended Codec | Reason |
|---|---|---|---|
| RP2350 (Cortex-M33) | Yes | XAP | FPU + DSP extensions |
| ESP32-S3 (Xtensa LX7) | Yes | XAP | FPU + PIE SIMD |
| STM32F411 (Cortex-M4F) | Yes | XAP | FPU + DSP extensions |
| nRF52840 (Cortex-M4F) | Yes | XAP | FPU + DSP extensions |
| nRF9160 (Cortex-M33) | Yes | XAP | FPU + DSP extensions |
| STM32F103 (Cortex-M3) | No | ADPCM | No FPU |
| ESP32-C3 (RISC-V) | No | ADPCM | No FPU, limited RAM |
| RP2040 (Cortex-M0+) | No | ADPCM | No FPU |
Codec Comparison¶
| XAP | IMA-ADPCM | |
|---|---|---|
| Compression | 8:1--10:1 | 4:1 |
| Quality | Excellent (spectral) | Fair (time-domain) |
| Bandwidth (4ch @96kHz) | ~30--40 KB/s | ~192 KB/s |
| FPU required | Yes | No |
| CPU | ~10 MIPS/ch | < 1 MIPS/ch |
| RAM | ~8 KB/ch | ~3 B/ch |
| LTE-M1 viable (4ch @96kHz) | Yes | No (exceeds budget) |
| Use case | Production (FPU targets) | Constrained fallback |
Legacy C SDK¶
The original C SDK is preserved at sdk/c/ for existing deployments.
sdk/c/
common/ # Platform-independent core (XAP, ADPCM, XMBP, ring buffer, client, session, transport)
pico/ # RP2350 (Raspberry Pi Pico 2) platform layer + examples
rp2040/ # RP2040 (Raspberry Pi Pico 1) platform layer + examples
esp32/ # ESP32-S3/C3 platform layer + examples
stm32/ # STM32F103/F411/WB55/WBA55 platform layer + examples
stm32u5/ # STM32U5 ultra-low-power platform layer + examples
nrf/ # nRF52840/nRF9160 platform layer + examples
test/ # Cross-platform unit tests + Docker harness (XMBP, XAP, ADPCM, ring buffer, client, session, cross-parity)
SDK Requirements¶
- Rust toolchain: Latest stable Rust (2024 edition) + Embassy framework +
embedded-hal1.0 - Cross-compile targets:
thumbv7m-none-eabi,thumbv7em-none-eabihf,thumbv8m.main-none-eabihf,riscv32imc-unknown-none-elf,xtensa-esp32s3-none-elf - Flash/debug tools:
probe-rs-tools(ARM targets),espflash(ESP32 targets) - Legacy C build: GCC (native) or
gcc-arm-none-eabi(cross-compile), CMake 3.20+ - Legacy C test: Docker with
qemu-system-armandqemu-user-staticfor ARM validation - Platform SDKs (legacy C only): Pico SDK (RP2350), ESP-IDF (ESP32), STM32CubeF1/F4/WB (STM32), Zephyr (nRF)
Metadata API¶
The client exposes per-stream feed methods for pushing sensor samples into the internal accumulation buffer. The SDK flushes buffered samples to the server automatically at meta_batch_ms intervals (default 1000 ms), or when flush_meta() is called explicitly.
Feed Methods¶
| Method | Stream Type | Wire Tag | Sample Size | Description |
|---|---|---|---|---|
meta_feed_f32(stream_index, value: f32) |
F32 | 0x01 | 12 bytes | General-purpose float sensor value |
meta_feed_i32(stream_index, value: i32) |
F32 (cast) | 0x01 | 12 bytes | Integer sensor value, stored as f32 |
meta_feed_i16(stream_index, value: i16) |
I16 | 0x0B | 10 bytes | Compact 16-bit integer (saves 17% vs F32) |
meta_feed_i8(stream_index, value: i8) |
I8 | 0x0C | 9 bytes | Compact 8-bit integer (saves 25% vs F32) |
meta_feed_f32_array(stream_index, values: &[f32]) |
F32 | 0x01 | 12 bytes each | Feed consecutive streams from a slice |
meta_feed_batch_f32(indices: &[u16], values: &[f32]) |
F32 | 0x01 | 12 bytes each | Feed arbitrary stream indices from parallel slices |
flush_meta() |
— | — | — | Flush all buffered samples immediately |
The stream type is set automatically by the feed method called. Calling meta_feed_i16 on a stream marks that stream as I16 for the entire batch; calling meta_feed_f32 on the same stream in the same batch would overwrite the type. Keep each stream to a single feed method per batch.
meta_feed_i16 — Compact 16-bit Integer Samples¶
Use meta_feed_i16 when the sensor produces 10–16 bit integer ADC counts and floating-point conversion would waste bandwidth. The value is stored as a big-endian i16 on the wire (wire tag 0x0B, 2-byte value, 10 bytes total per sample).
Typical use cases: ADXL345 accelerometer raw axis counts (10–13 bit), raw ADC readings from any sensor where unit conversion happens server-side.
// ADXL345: read raw 16-bit axis values, feed directly without float conversion
let (x_raw, y_raw, z_raw): (i16, i16, i16) = adxl345_read_raw(&mut spi);
client.meta_feed_i16(0, x_raw)?; // stream 0: X axis
client.meta_feed_i16(1, y_raw)?; // stream 1: Y axis
client.meta_feed_i16(2, z_raw)?; // stream 2: Z axis
The session must declare streams 0–2 with value_type: "i16" when calling POST /api/v1/ingest/sessions.
meta_feed_i8 — Compact 8-bit Integer Samples¶
Use meta_feed_i8 when data fits in 8 bits (range −128 to 127). The value is stored as a single byte on the wire (wire tag 0x0C, 1-byte value, 9 bytes total per sample — same byte count as Bool).
Typical use cases: Normalized vibration index, coarse temperature delta (tenths of a degree offset from a base), status codes that fit in a signed byte.
// Coarse temperature delta encoded as i8 (e.g., offset from 20 °C in 0.5 °C steps)
let temp_raw: i16 = cht832x_read_raw_temp(&mut i2c); // 14-bit count
let delta_steps = ((temp_raw as f32 * 0.0625) - 20.0) / 0.5; // offset from 20°C in 0.5°C steps
client.meta_feed_i8(3, delta_steps.clamp(-128.0, 127.0) as i8)?;
Bandwidth Comparison¶
For a 4-axis accelerometer at 500 Hz on Cat-M1 (~37 KB/s uplink):
| Type | Bytes/sample | Bytes/sec (4 axes × 500 Hz) | % of 37 KB/s budget |
|---|---|---|---|
| F32 | 12 | 24,000 | 63% |
| I16 | 10 | 20,000 | 53% |
| I8 | 9 | 18,000 | 47% |
Using i16 instead of f32 for raw accelerometer data frees ~4 KB/s — enough to add another slow sensor stream (temperature, humidity) without exceeding the LTE-M1 budget.
Health Reporting¶
The Health struct tracks runtime statistics:
| Field | Type | Description |
|---|---|---|
uptime_sec |
u32 | Device uptime |
audio_batches_sent |
u32 | Successful audio batches |
meta_batches_sent |
u32 | Successful metadata batches |
batches_failed |
u32 | Failed transmissions |
retries_total |
u32 | Total retry count |
reconnects |
u32 | Session reconnects |
samples_dropped |
u32 | Samples lost to buffer overflow |
last_batch_seq |
u16 | Last batch sequence number |
rssi_dbm |
i8 | Signal strength |
Health reports are sent to the server every health_interval_sec (default 300s).
Error Handling¶
SDK errors (SdkError) map 1:1 to the C SDK xylolabs_err_t:
| Error | Meaning |
|---|---|
Network |
Connection failed, send/recv error |
Timeout |
Operation timed out |
Auth |
Invalid API key (401/403) |
BufferFull |
Ring buffer or metadata overflow |
InvalidState |
Operation not valid in current state |
Protocol |
Malformed response |
Server |
HTTP 5xx |
NoMemory |
Buffer allocation failed |
InvalidParam |
Bad function parameter |
NotInitialized |
SDK not initialized |