Skip to content

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-*) and sdk/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: SessionManagerIDLE → 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) and XMBP_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: - IDLECONNECTINGACTIVECLOSING - 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.

  • N must be a power of 2 (compile-time assertion).
  • Uses AtomicUsize with Release/Acquire memory ordering for multi-core safety.
  • UnsafeCell for 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-hal 1.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-arm and qemu-user-static for 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