Skip to content

XMBP v2 Implementation Plan

Status: Draft
Author: Engineering
Date: 2026-04-05
Scope: Wire protocol, Rust encoder/decoder, C SDK encoder, server ingest, XMCH storage


1. Motivation

XMBP v1 was designed for simplicity on bare-metal targets. Every sample carries a full 8-byte absolute timestamp regardless of data type, which is wasteful for regular-interval sensor data. The analysis below quantifies the problem:

Scenario Payload Timestamps Efficiency
400× F32 metadata (4 streams, 100 samples) 1,600 B 3,200 B 33.1%
50× Bool sensor 50 B 400 B 10.8%
50× XAP audio frames (~160 B each) 8,000 B 400 B 93.9%

Sensor/metadata workloads — the majority of XMBP traffic — waste 66–89% of bandwidth on timestamps. On LTE-M1 links (typical 10–30 KB/s uplink), this directly translates to higher latency, more retransmissions, and shorter battery life.

Goals

  1. 50–75% bandwidth reduction for sensor metadata workloads
  2. Zero regression for audio (XAP/ADPCM) workloads
  3. Zero additional RAM on encoder side — no compression libraries, no extra buffers
  4. Full backward compatibility — v1 batches remain valid indefinitely
  5. Incremental rollout — server accepts v1 and v2 concurrently

Non-Goals

  • Compression (zstd, LZ4, etc.) — MCU targets are too resource-constrained; the FLAG_ZSTD_COMPRESSED (0x02) remains reserved for future server-to-server use only
  • Encryption at protocol level — handled by TLS transport
  • Streaming/chunked transfer — batch model is sufficient for current workloads

2. Version Negotiation & Backward Compatibility

2.1 Version Field

The existing version byte (offset 4) changes from 0x01 to 0x02. The server decoder inspects this byte first and dispatches to the appropriate parser.

version == 0x01  →  v1 decoder (current, unchanged)
version == 0x02  →  v2 decoder (new)
version >= 0x03  →  UnsupportedVersion error

2.2 Rollout Strategy

Phase Server SDK Duration
Phase 0 Decode v1 only (current) Encode v1 only (current) Baseline
Phase 1 Decode v1 + v2 Encode v1 (default) Deploy server first
Phase 2 Decode v1 + v2 Encode v2 (default), v1 opt-in OTA firmware update
Phase 3 Decode v1 + v2 Encode v2 only Deprecate v1 in SDK

The SDK exposes a protocol_version config field (default: 2 after Phase 2). Devices with older firmware continue sending v1 indefinitely.

2.3 Feature Detection

No capability negotiation is needed. The version byte is self-describing per batch. A single device can even send a mix of v1 and v2 batches during firmware transition.


3. Wire Format Changes

3.1 Batch Envelope (v2)

Offset  Size  Field              Notes
------  ----  -----              -----
0       4     magic              0x584D4250 ("XMBP") — unchanged
4       1     version            0x02
5       1     flags              bit field — unchanged (see §3.2)
6       2     batch_seq          u16 BE — unchanged
[8]     [4]   device_id          u32 BE — conditional (FLAG_HAS_DEVICE_ID)
8/12    2     stream_count       u16 BE — unchanged

Base header size is unchanged (8–12 bytes). The difference is in stream block encoding.

3.2 Flags Field (v2)

Bit Constant Hex Status
0 FLAG_HAS_DEVICE_ID 0x01 Unchanged
1 FLAG_ZSTD_COMPRESSED 0x02 Reserved (not used by MCU encoders)
2–7 Reserved Must be zero

No new flags are introduced. All v2 improvements are structural (columnar layout, timestamp modes) and require no flag signaling — the version byte is sufficient.

3.3 Stream Block Format (v2)

Offset  Size  Field              Notes
------  ----  -----              -----
0       2     stream_id          u16 BE — unchanged
2       1     value_type         u8 — unchanged (0x00–0x0D)
3       1     timestamp_mode     u8 — NEW (see §3.4)
4       2     sample_count       u16 BE — unchanged
6       var   timestamp_section  encoding depends on timestamp_mode
var     var   value_section      encoding depends on value_type

Key change: Stream header grows from 5 to 6 bytes (+1 byte for timestamp_mode). Timestamps and values are now in separate columnar sections within each stream block, matching the XMCH storage layout.

3.4 Timestamp Modes

The timestamp_mode byte selects how the timestamp section is encoded:

Mode Constant Timestamp Section Layout Best For
0x00 TS_ABSOLUTE N × u64 BE (8 bytes each) Irregular events, v1 compat
0x01 TS_DELTA_U16 base_ts(u64 BE, 8B) + N × u16 BE delta (2B each) Regular ≤65 ms span
0x02 TS_DELTA_U32 base_ts(u64 BE, 8B) + N × u32 BE delta (4B each) Regular ≤~71 min span
0x03 TS_IMPLICIT start_ts(u64 BE, 8B) + interval_us(u32 BE, 4B) Fixed-rate sampling
0x80 TS_REF ref_stream_id(u16 BE, 2B) Multi-channel shared clock

3.4.1 TS_ABSOLUTE (0x00)

Identical to v1 but in columnar layout (all timestamps first, then all values).

[ts_0: u64 BE][ts_1: u64 BE]...[ts_N-1: u64 BE]

Size: N × 8 bytes.

3.4.2 TS_DELTA_U16 (0x01)

First timestamp is absolute. Subsequent timestamps are u16 microsecond deltas from the previous timestamp (not from base).

[base_ts: u64 BE][delta_0: u16 BE][delta_1: u16 BE]...[delta_N-1: u16 BE]
  • delta_0 is always 0 (base sample)
  • Reconstructed: ts[i] = ts[i-1] + delta[i] for i > 0; ts[0] = base_ts
  • Max span per delta: 65,535 µs (~65.5 ms)
  • Size: 8 + N × 2 bytes

Encoder rule: If any consecutive delta exceeds 65,535 µs, the encoder must fall back to TS_DELTA_U32 or TS_ABSOLUTE.

3.4.3 TS_DELTA_U32 (0x02)

Same as TS_DELTA_U16 but with 4-byte deltas.

[base_ts: u64 BE][delta_0: u32 BE][delta_1: u32 BE]...[delta_N-1: u32 BE]
  • Max span per delta: 4,294,967,295 µs (~71.6 minutes)
  • Size: 8 + N × 4 bytes

3.4.4 TS_IMPLICIT (0x03)

For fixed-rate streams. All timestamps are derived from start + index × interval.

[start_ts: u64 BE][interval_us: u32 BE]
  • Reconstructed: ts[i] = start_ts + i × interval_us
  • Size: 12 bytes (constant, regardless of sample_count)
  • Constraint: interval_us > 0

Encoder rule: Only valid when the encoder knows the sampling interval is perfectly regular (e.g., timer-driven ADC). The SDK auto-selects this mode when Config::audio_sample_rate or Config::meta_sample_rate is set and jitter is below a configurable threshold (timestamp_jitter_tolerance_us, default: 10 µs).

3.4.5 TS_REF (0x80)

References another stream's timestamps within the same batch.

[ref_stream_id: u16 BE]
  • Size: 2 bytes (constant, regardless of sample_count)
  • The referenced stream must appear earlier in the batch (forward references are invalid; the decoder validates this)
  • The referenced stream's sample_count must equal this stream's sample_count

Use case: Multi-channel audio (4ch XAP) where all channels share the same sample clock. The first channel encodes timestamps normally; channels 2–4 use TS_REF pointing to channel 0.

3.5 Value Section

Values are written contiguously without per-sample timestamp prefixes (since timestamps are in the separate timestamp section).

Fixed-size types:

[val_0][val_1]...[val_N-1]
Size: N × value_size (e.g., N × 4 for F32, N × 1 for Bool)

Variable-size types:

[len_0: u16/u32 BE][data_0][len_1: u16/u32 BE][data_1]...
Length prefix: u16 for String/Bytes/Arrays, u32 for Json (unchanged from v1).

3.6 Savings Summary

Scenario v1 Size v2 (delta u16) v2 (implicit) v2 (implicit + TS_REF)
400× F32 (4 streams, 100ea) 4,834 B 2,844 B (−41%) 2,468 B (−49%) 2,468 B (−49%)
50× Bool (1 stream) 465 B 165 B (−65%) 68 B (−85%) 68 B (−85%)
50× XAP frames (~160B ea) 8,515 B 8,215 B (−4%) 8,127 B (−5%) 8,127 B (−5%)
4ch F32 shared clock (100ea) 4,834 B 2,244 B (−54%) 1,868 B (−61%) 1,646 B (−66%)
4ch audio (100 XAP frames ea) 34,060 B 32,860 B (−4%) 32,508 B (−5%) 32,286 B (−5%)

4. New Value Types

4.1 U8 (Tag 0x0B)

Single unsigned byte. Common for status codes, GPIO states, enum values.

  • Wire tag: 0x0B
  • Value size: 1 byte
  • Motivation: Bool is semantically wrong for multi-state values (0–255)

4.2 F16 (Tag 0x0C)

IEEE 754 half-precision float. Useful for low-precision sensor data (temperature, humidity) where ±0.1 resolution suffices.

  • Wire tag: 0x0C
  • Value size: 2 bytes
  • Range: ±65,504, ~3.3 decimal digits
  • Motivation: 50% smaller than F32 for coarse sensor readings

4.3 I16 (Tag 0x0D)

Signed 16-bit integer.

  • Wire tag: 0x0D
  • Value size: 2 bytes
  • Range: −32,768 to 32,767
  • Motivation: Raw ADC values (12-bit, 14-bit) fit in I16, wasting 2 bytes per sample with I32

4.4 Updated Value Type Registry

Tag Type Fixed Size Length Prefix Status
0x00 F64 8 Unchanged
0x01 F32 4 Unchanged
0x02 I64 8 Unchanged
0x03 I32 4 Unchanged
0x04 Bool 1 Unchanged
0x05 String var u16 BE Unchanged
0x06 Bytes var u16 BE Unchanged
0x07 F64Array var u16 BE Unchanged
0x08 F32Array var u16 BE Unchanged
0x09 I32Array var u16 BE Unchanged
0x0A Json var u32 BE Unchanged
0x0B U8 1 New in v2
0x0C F16 2 New in v2
0x0D I16 2 New in v2
0x0E–0xFF Reserved

4.5 Combined Impact: New Types + Timestamp Modes

Smaller value types compound with timestamp savings:

Scenario (100 samples, 1 stream) v1 (F32 + absolute) v2 (F16 + implicit) Reduction
Temperature readings 1,215 B 218 B −82%
GPIO state log 915 B (Bool) 118 B (U8 + implicit) −87%
12-bit ADC values 1,215 B (I32) 218 B (I16 + implicit) −82%

5. Shared Timestamp Optimization (TS_REF)

5.1 Problem

Multi-channel audio (4ch XAP) produces 4 stream blocks with identical timestamp sequences. In v1, each stream carries its own timestamps: 4 × N × 8 bytes of redundant data.

5.2 Solution

TS_REF (0x80) allows a stream block to reference another stream's timestamps instead of encoding its own. The timestamp section is just a 2-byte stream_id pointing to a previously decoded stream block in the same batch.

5.3 Savings

For 4ch audio, 100 samples per channel: - v1: 4 × 100 × 8 = 3,200 B of timestamps - v2 with TS_REF: 1 × (8 + 100×2) + 3 × 2 = 214 B (delta u16) or 1 × 12 + 3 × 2 = 18 B (implicit) - Savings: 93–99% of timestamp overhead for multi-channel streams

5.4 Constraints

  • Referenced stream must appear earlier in the batch (no forward references)
  • Referenced stream's sample_count must match
  • Decoder validates both constraints and returns InvalidTimestampRef on violation
  • Chains are allowed (stream 2 → stream 1 → stream 0) but the decoder resolves to the root timestamp array, not intermediate references

6. Timestamp Mode Selection Algorithm

The SDK encoder uses this decision tree (both Rust and C):

Input: timestamps[0..N], tolerance_us

1. If N <= 1:
     → TS_ABSOLUTE (no savings possible)

2. Compute intervals: intervals[i] = timestamps[i+1] - timestamps[i]

3. If all intervals == intervals[0] (within ±tolerance_us):
     → TS_IMPLICIT(start_ts=timestamps[0], interval_us=intervals[0])

4. Compute max_delta = max(intervals)
   If max_delta <= 65535:
     → TS_DELTA_U16

5. If max_delta <= 4294967295:
     → TS_DELTA_U32

6. Else:
     → TS_ABSOLUTE

For multi-channel streams sharing the same sample clock:

7. If stream[i].timestamps == stream[0].timestamps:
     → TS_REF(ref_stream_id=stream[0].stream_id)

RAM cost: One pass over the timestamp array. No additional buffers needed — deltas are computed and written in a single streaming pass.


7. Implementation Plan

Phase 1: Protocol Crate (xylolabs-protocol) — Foundation

Estimated scope: ~600 lines Rust

7.1.1 Constants (constants.rs)

  • Add XMBP_VERSION_2: u8 = 2
  • Add timestamp mode constants:
  • TS_ABSOLUTE: u8 = 0x00
  • TS_DELTA_U16: u8 = 0x01
  • TS_DELTA_U32: u8 = 0x02
  • TS_IMPLICIT: u8 = 0x03
  • TS_REF: u8 = 0x80
  • Add STREAM_HEADER_SIZE_V2: usize = 6 (stream_id + value_type + ts_mode + sample_count)
  • Add value size constants: SAMPLE_U8_SIZE: usize = 1, SAMPLE_F16_SIZE: usize = 2, SAMPLE_I16_SIZE: usize = 2
  • Add batch_size_v2() calculator that accounts for timestamp mode overhead

7.1.2 Value Types (value_type.rs)

  • Add variants: U8, F16, I16
  • Assign wire tags 0x0B, 0x0C, 0x0D
  • Update from_wire_tag() to accept 0x0B–0x0D
  • Update fixed_value_size() for new types
  • Backward compat: v1 decoder continues to reject tags > 0x0A

7.1.3 Error Types (error.rs)

  • Add InvalidTimestampMode(u8) — unknown mode byte
  • Add InvalidTimestampRef(u16) — forward reference or missing stream
  • Add TimestampOverflow — delta reconstruction exceeds u64
  • Add SampleCountMismatch — TS_REF target has different sample_count

7.1.4 Encoder (encode.rs)

New methods on XmbpWriter:

// v2 batch header (writes version=0x02)
fn write_batch_header_v2(&mut self, flags: u8, batch_seq: u16);

// v2 stream block header (6 bytes)
fn begin_stream_v2(
    &mut self,
    stream_id: u16,
    value_type: MetadataValueType,
    timestamp_mode: u8,
    sample_count: u16,
);

// Timestamp section writers
fn write_timestamps_absolute(&mut self, timestamps: &[u64]);
fn write_timestamps_delta_u16(&mut self, base_ts: u64, deltas: &[u16]);
fn write_timestamps_delta_u32(&mut self, base_ts: u64, deltas: &[u32]);
fn write_timestamps_implicit(&mut self, start_ts: u64, interval_us: u32);
fn write_timestamps_ref(&mut self, ref_stream_id: u16);

// Value section writers (columnar, no timestamps interleaved)
fn write_values_f32(&mut self, values: &[f32]);
fn write_values_f64(&mut self, values: &[f64]);
fn write_values_i32(&mut self, values: &[i32]);
fn write_values_i64(&mut self, values: &[i64]);
fn write_values_bool(&mut self, values: &[bool]);
fn write_values_u8(&mut self, values: &[u8]);
fn write_values_f16(&mut self, values: &[u16]);  // IEEE 754 half, passed as raw u16 bits
fn write_values_i16(&mut self, values: &[i16]);

// High-level v2 convenience builder
fn build_f32_batch_v2(
    &mut self,
    batch_seq: u16,
    device_id: u32,
    num_streams: u16,
    sample_count: u16,
    start_ts: u64,
    interval_us: u32,     // 0 = use absolute timestamps from array
    timestamps: &[u64],   // ignored if interval_us > 0
    values: &[f32],
    values_stride: u16,
) -> usize;

All v1 methods remain unchanged and functional.

7.1.5 Decoder (decode.rs)

  • XmbpReader::decode() inspects version byte:
  • 0x01 → existing v1 path (unchanged)
  • 0x02 → new decode_v2() path
  • decode_v2():
  • Parse batch header (same as v1)
  • Parse stream_count
  • For each stream block: a. Read 6-byte v2 stream header (including timestamp_mode) b. Decode timestamp section based on timestamp_mode c. For TS_REF: look up referenced stream, validate constraints d. Decode value section based on value_type e. Reconstruct Sample structs (absolute timestamp + value) f. Store as StreamBlock (same output struct as v1)
  • Output struct is identical to v1XmbpBatch / StreamBlock / Sample types don't change. The decoder reconstructs absolute timestamps internally. Downstream code (ingest manager, DB storage) requires zero changes.

Phase 2: C SDK Encoder (sdk/c/)

Estimated scope: ~300 lines C

7.2.1 Header (xmbp_encoder.h)

  • Add timestamp mode constants: XMBP_TS_ABSOLUTE, XMBP_TS_DELTA_U16, XMBP_TS_DELTA_U32, XMBP_TS_IMPLICIT, XMBP_TS_REF
  • Add value type constants: XMBP_VT_U8 = 11, XMBP_VT_F16 = 12, XMBP_VT_I16 = 13
  • Add XMBP_STREAM_HEADER_SIZE_V2 = 6
  • Add v2 function prototypes mirroring the Rust encoder:
  • xmbp_write_batch_header_v2()
  • xmbp_begin_stream_v2()
  • xmbp_write_timestamps_absolute(), _delta_u16(), _delta_u32(), _implicit(), _ref()
  • xmbp_write_values_f32(), _f64(), _i32(), _i64(), _bool(), _u8(), _f16(), _i16()
  • xmbp_build_f32_batch_v2()
  • All v1 functions remain unchanged

7.2.2 Implementation (xmbp_encoder.c)

  • Implement all v2 functions using existing write_*_be() primitives
  • Add write_f16_be() — IEEE 754 half-precision via bit manipulation (no libm)
  • Add write_i16_be() — cast + write_u16_be()
  • The v2 batch builder auto-selects the best timestamp mode:
  • If interval_us > 0TS_IMPLICIT
  • Else compute deltas; if max fits u16 → TS_DELTA_U16
  • Else if max fits u32 → TS_DELTA_U32
  • Fallback → TS_ABSOLUTE

7.2.3 Tests (test_xmbp.c)

New test functions: - test_v2_batch_header — verify version byte is 0x02 - test_v2_stream_header — verify 6-byte header with timestamp_mode - test_ts_absolute — absolute timestamps in columnar layout - test_ts_delta_u16 — delta encoding with correct byte output - test_ts_delta_u32 — delta encoding with larger spans - test_ts_implicit — implicit timestamps with interval - test_ts_ref — cross-stream timestamp reference (2-byte output) - test_sample_u8 — U8 value type roundtrip - test_sample_f16 — F16 value type roundtrip - test_sample_i16 — I16 value type roundtrip - test_v2_build_f32_batch — convenience builder with auto timestamp mode

Phase 3: SDK Integration (xylolabs-sdk)

Estimated scope: ~150 lines Rust

7.3.1 Config (config.rs)

New fields:

pub protocol_version: u8,              // Default: 2
pub timestamp_jitter_tolerance_us: u32, // Default: 10

Validation: - protocol_version must be 1 or 2 - timestamp_jitter_tolerance_us must be < 1,000,000

7.3.2 Session/Transport

  • The session encoder checks protocol_version and calls v1 or v2 writer
  • Auto timestamp mode selection logic (§6):
  • Audio streams with known sample rate → TS_IMPLICIT
  • Multi-channel audio → first channel gets TS_IMPLICIT, rest get TS_REF
  • Metadata with regular timer → TS_DELTA_U16 or TS_IMPLICIT
  • Irregular events → TS_ABSOLUTE

Phase 4: Server Ingest (xylolabs-server)

Estimated scope: ~100 lines Rust

7.4.1 Ingest Route (routes/ingest.rs)

  • No changes needed — XmbpReader::decode() dispatches internally by version
  • The decoded XmbpBatch struct is identical for v1 and v2

7.4.2 Ingest Manager (ingest/manager.rs)

  • Add metric: xmbp_v2_batches_total counter for monitoring rollout
  • Log protocol version per batch at debug level

7.4.3 Validation

  • Reject v2 batches with unknown timestamp modes (not in {0x00–0x03, 0x80})
  • Validate TS_REF references point to already-decoded streams
  • Validate TS_REF target has matching sample_count
  • Validate TS_IMPLICIT has interval_us > 0
  • Validate delta reconstruction doesn't overflow u64

Phase 5: XMCH Storage Format

Estimated scope: ~80 lines Rust

7.5.1 Chunk Format (chunk_format.rs)

  • XMCH is already column-oriented — no structural changes needed
  • Add support for new value types (U8, F16, I16) in encode_values_into() and decode_values()
  • XMCH version remains 0x01 (new value types are additive to the registry)

Phase 6: Documentation & Specification

7.6.1 XMBP-SPECIFICATION.md

  • Add "Version 2" section with full wire format documentation
  • Document all 5 timestamp modes with byte-level diagrams
  • Document new value types (U8, F16, I16)
  • Add v1 ↔ v2 migration guide
  • Update wire format examples with v2 batches

7.6.2 XMBP-SPECIFICATION.ko.md

  • Korean translation of all v2 additions

7.6.3 Platform Docs

  • Update PLATFORM-NRF.md, PLATFORM-ESP32.md with v2 memory impact (negligible)
  • Update example READMEs with v2 configuration

8. Detailed Wire Format Examples

8.1 v2 Batch: 1 F32 Stream, 4 Samples, TS_IMPLICIT

Offset  Hex                          Field
------  ---                          -----
0       58 4D 42 50                  magic "XMBP"
4       02                           version 2
5       01                           flags (HAS_DEVICE_ID)
6       00 2A                        batch_seq = 42
8       00 00 00 07                  device_id = 7
12      00 01                        stream_count = 1
                                     --- stream 0 ---
14      00 00                        stream_id = 0
16      01                           value_type = F32
17      03                           timestamp_mode = TS_IMPLICIT
18      00 04                        sample_count = 4
                                     --- timestamp section ---
20      00 00 01 93 A8 B0 00 00      start_ts = 1735689600000000 µs
28      00 00 03 E8                  interval_us = 1000 (1 kHz)
                                     --- value section ---
32      41 BC 00 00                  val[0] = 23.5f
36      41 C8 00 00                  val[1] = 25.0f
40      41 A0 00 00                  val[2] = 20.0f
44      41 B0 00 00                  val[3] = 22.0f

Total: 48 bytes
v1 equivalent: 8 + 4 + 2 + 5 + 4×(8+4) = 67 bytes
Savings: 19 bytes (−28%)

8.2 v2 Batch: 4ch F32, Shared Clock, TS_REF

Offset  Field
------  -----
0–11    batch header (version=2, flags=0x01, device_id=7)
12–13   stream_count = 4
                                     --- stream 0 (reference) ---
14      stream_id=0, value_type=F32, ts_mode=TS_IMPLICIT, count=100
20      start_ts(8B) + interval_us(4B) = 12 bytes timestamps
32      100 × f32 = 400 bytes values
                                     --- stream 1 (ref → stream 0) ---
432     stream_id=1, value_type=F32, ts_mode=TS_REF, count=100
438     ref_stream_id=0 = 2 bytes timestamps
440     100 × f32 = 400 bytes values
                                     --- stream 2 (ref → stream 0) ---
840     stream_id=2, ... ts_mode=TS_REF ... ref=0
848     100 × f32 = 400 bytes values
                                     --- stream 3 (ref → stream 0) ---
1248    stream_id=3, ... ts_mode=TS_REF ... ref=0
1256    100 × f32 = 400 bytes values

Total: 1,656 bytes
v1 equivalent: 14 + 4×(5 + 100×12) = 4,834 bytes
Savings: 3,178 bytes (−66%)

8.3 v2 Batch: Bool Stream, TS_DELTA_U16

Offset  Field
------  -----
0–9     batch header (version=2, no device_id)
10–11   stream_count = 1
                                     --- stream 0 ---
12      stream_id=0, value_type=Bool, ts_mode=TS_DELTA_U16, count=50
18      base_ts(8B) + 50 × u16 delta = 108 bytes timestamps
126     50 × bool = 50 bytes values

Total: 176 bytes
v1 equivalent: 10 + 5 + 50×9 = 465 bytes
Savings: 289 bytes (−62%)

9. Migration Checklist

Server (deploy first)

  • [ ] Add v2 decoder path in XmbpReader::decode()
  • [ ] Add new value types to MetadataValueType (U8, F16, I16)
  • [ ] Add timestamp mode parsing and reconstruction
  • [ ] Add TS_REF validation (no forward references, matching sample_count)
  • [ ] Add xmbp_v2_batches_total Prometheus metric
  • [ ] Integration tests: v2 batches through full ingest pipeline
  • [ ] Regression tests: all existing v1 tests pass unchanged

Rust SDK

  • [ ] Add v2 encoder methods to XmbpWriter
  • [ ] Add protocol_version config field
  • [ ] Add timestamp_jitter_tolerance_us config field
  • [ ] Implement auto timestamp mode selection (§6)
  • [ ] Add TS_REF for multi-channel audio
  • [ ] Unit tests for all 5 timestamp modes
  • [ ] Cross-test: Rust-encoded v2 decoded by server

C SDK

  • [ ] Add v2 encoder functions to xmbp_encoder.h / .c
  • [ ] Implement auto timestamp mode selection
  • [ ] Add new value type writers (U8, F16, I16)
  • [ ] Unit tests for all v2 features
  • [ ] Cross-parity test: C-encoded v2 decoded by Rust decoder

XMCH Storage

  • [ ] Add U8, F16, I16 to encode_values_into() and decode_values()
  • [ ] Roundtrip tests for new value types

Documentation

  • [ ] Update XMBP-SPECIFICATION.md (EN + KO)
  • [ ] Update platform guides (NRF, ESP32, STM32, RP2350)
  • [ ] Update example READMEs
  • [ ] Update AGENTS.md with v2 protocol notes

10. Risk Assessment

Risk Likelihood Impact Mitigation
F16 precision insufficient for some sensors Low Low F16 is additive; existing F32 remains default; document precision limits
TS_REF decoding order dependency Low Medium Validate at decode time; reject forward references with clear error
v1/v2 mixed batches in same session Low Low Server handles both; no session-level version state
TS_DELTA overflow mid-stream Low Low Encoder validates all deltas before committing mode; falls back gracefully
Older firmware sends unknown value types Low Low Server rejects unknown tags with InvalidValueType; device gets HTTP 400

11. Performance Targets

Metric Target Measurement
v2 encode time (Cortex-M4F, 100 F32 samples) < 200 µs Benchmark on STM32F411
v2 encode time (ESP32-S3, 100 F32 samples) < 100 µs Benchmark on ESP32-S3
v2 decode time (server, 1000 F32 samples) < 50 µs Benchmark in CI
Metadata bandwidth reduction (4 streams, 100 samples) ≥ 49% Automated test assertion
Multi-ch bandwidth reduction (4ch, TS_REF) ≥ 60% Automated test assertion
Audio bandwidth regression < 2% Automated test assertion
Additional encoder RAM 0 bytes No new buffers; single-pass streaming write

12. Testing Strategy

Unit Tests

  • Each timestamp mode: encode → decode roundtrip with exact value comparison
  • Each new value type: encode → decode roundtrip (U8, F16, I16)
  • TS_REF: valid back-reference → success; forward reference → InvalidTimestampRef
  • TS_REF: sample_count mismatch → SampleCountMismatch
  • TS_IMPLICIT: interval_us=0 → error
  • TS_DELTA_U16: delta > 65535 → encoder falls back to TS_DELTA_U32
  • Overflow: v2 writer overflow detection (same behavior as v1)
  • Edge cases: empty batch, single sample, max sample_count (65535), N=1 with each mode

Cross-Parity Tests

  • C SDK v2 encode → Rust v2 decode (byte-identical validation)
  • v1 batches through v2-aware decoder → identical output to v1 decoder

Integration Tests

  • Full pipeline: v2 encode → HTTP POST → server decode → DB store → API query
  • Mixed v1/v2 batches in same ingest session
  • 4ch audio with TS_REF through full pipeline

Performance Tests

  • Encode/decode benchmarks at 100, 1,000, 10,000 samples
  • Compare v1 vs v2 encode time (v2 should be equal or faster — fewer bytes to write)
  • Memory profiling: confirm zero additional heap/stack on MCU targets

Regression Tests

  • All existing v1 C and Rust tests pass unchanged
  • v1 encode → v2-aware decoder produces identical XmbpBatch output