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¶
- 50–75% bandwidth reduction for sensor metadata workloads
- Zero regression for audio (XAP/ADPCM) workloads
- Zero additional RAM on encoder side — no compression libraries, no extra buffers
- Full backward compatibility — v1 batches remain valid indefinitely
- 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).
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).
delta_0is 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 × 2bytes
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.
- Max span per delta: 4,294,967,295 µs (~71.6 minutes)
- Size:
8 + N × 4bytes
3.4.4 TS_IMPLICIT (0x03)¶
For fixed-rate streams. All timestamps are derived from start + index × interval.
- Reconstructed:
ts[i] = start_ts + i × interval_us - Size:
12bytes (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.
- Size:
2bytes (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_countmust equal this stream'ssample_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:
Size:N × value_size (e.g., N × 4 for F32, N × 1 for Bool)
Variable-size types:
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_countmust match - Decoder validates both constraints and returns
InvalidTimestampRefon 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:
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 = 0x00TS_DELTA_U16: u8 = 0x01TS_DELTA_U32: u8 = 0x02TS_IMPLICIT: u8 = 0x03TS_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→ newdecode_v2()pathdecode_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 ontimestamp_modec. ForTS_REF: look up referenced stream, validate constraints d. Decode value section based onvalue_typee. ReconstructSamplestructs (absolute timestamp + value) f. Store asStreamBlock(same output struct as v1) - Output struct is identical to v1 —
XmbpBatch/StreamBlock/Sampletypes 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 > 0→TS_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:
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_versionand 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 getTS_REF - Metadata with regular timer →
TS_DELTA_U16orTS_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
XmbpBatchstruct is identical for v1 and v2
7.4.2 Ingest Manager (ingest/manager.rs)¶
- Add metric:
xmbp_v2_batches_totalcounter 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_REFreferences point to already-decoded streams - Validate
TS_REFtarget has matchingsample_count - Validate
TS_IMPLICIThasinterval_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()anddecode_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_REFvalidation (no forward references, matching sample_count) - [ ] Add
xmbp_v2_batches_totalPrometheus 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_versionconfig field - [ ] Add
timestamp_jitter_tolerance_usconfig field - [ ] Implement auto timestamp mode selection (§6)
- [ ] Add
TS_REFfor 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()anddecode_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
XmbpBatchoutput