|
| 1 | +//! Framing Protocol Buffers over a serial link with COBS. |
| 2 | +//! |
| 3 | +//! Protobuf is a *serialization* format, not a *wire* format: it turns a message |
| 4 | +//! into a compact byte string but says nothing about where one message ends and |
| 5 | +//! the next begins. Push raw protobuf bytes down a UART and the receiver sees an |
| 6 | +//! undifferentiated stream. COBS supplies the missing framing: |
| 7 | +//! |
| 8 | +//! 1. **Serialize** the message with protobuf (the bytes usually contain `0x00`). |
| 9 | +//! 2. **COBS-encode** it so the payload contains no `0x00`, then append a single |
| 10 | +//! `0x00` **delimiter**. That byte now unambiguously marks the end of a frame. |
| 11 | +//! 3. On the far end, split the stream on `0x00`, COBS-decode each frame, and hand |
| 12 | +//! the bytes back to protobuf. |
| 13 | +//! |
| 14 | +//! The payoff over length-prefixing: if line noise corrupts a packet, the receiver |
| 15 | +//! just waits for the next `0x00` and **resynchronises on the very next message**, |
| 16 | +//! discarding only the broken frame instead of losing sync forever. This example |
| 17 | +//! shows exactly that. |
| 18 | +//! |
| 19 | +//! Run it with `cargo run --example protobuf_cobs`. |
| 20 | +//! |
| 21 | +//! On a microcontroller you would use the allocation-free API — [`framing::frame`] |
| 22 | +//! to encode into a fixed `[u8; N]`, and [`framing::StreamDecoder`] to decode an |
| 23 | +//! incoming stream into another fixed buffer — for the same result with zero heap. |
| 24 | +//! |
| 25 | +//! [`framing::frame`]: cobs_codec_rs::framing::frame |
| 26 | +//! [`framing::StreamDecoder`]: cobs_codec_rs::framing::StreamDecoder |
| 27 | +#![allow(missing_docs)] |
| 28 | + |
| 29 | +use cobs_codec_rs::framing::{FrameDecoder, frame_to_vec}; |
| 30 | +use prost::Message; |
| 31 | + |
| 32 | +/// A telemetry packet a sensor node might stream to a host computer. |
| 33 | +#[derive(Clone, PartialEq, prost::Message)] |
| 34 | +struct SensorReading { |
| 35 | + #[prost(uint32, tag = "1")] |
| 36 | + node_id: u32, |
| 37 | + #[prost(float, tag = "2")] |
| 38 | + temperature_c: f32, |
| 39 | + #[prost(uint64, tag = "3")] |
| 40 | + uptime_ms: u64, |
| 41 | + #[prost(string, tag = "4")] |
| 42 | + label: String, |
| 43 | +} |
| 44 | + |
| 45 | +fn main() { |
| 46 | + let readings = [ |
| 47 | + SensorReading { |
| 48 | + node_id: 1, |
| 49 | + temperature_c: 21.5, |
| 50 | + uptime_ms: 1_000, |
| 51 | + label: "cabin".into(), |
| 52 | + }, |
| 53 | + SensorReading { |
| 54 | + node_id: 2, |
| 55 | + temperature_c: 100.0, |
| 56 | + uptime_ms: 1_256, |
| 57 | + label: "boiler".into(), |
| 58 | + }, |
| 59 | + SensorReading { |
| 60 | + node_id: 7, |
| 61 | + temperature_c: -4.25, |
| 62 | + uptime_ms: 99_999, |
| 63 | + label: "outside".into(), |
| 64 | + }, |
| 65 | + ]; |
| 66 | + |
| 67 | + // --- Device side: serialize each reading with protobuf, then COBS-frame it. --- |
| 68 | + let mut wire: Vec<u8> = Vec::new(); |
| 69 | + let mut frame_starts: Vec<usize> = Vec::new(); |
| 70 | + for reading in &readings { |
| 71 | + frame_starts.push(wire.len()); |
| 72 | + let payload = reading.encode_to_vec(); // protobuf bytes (the float alone brings 0x00s) |
| 73 | + wire.extend_from_slice(&frame_to_vec(&payload)); // COBS-encode + trailing 0x00 |
| 74 | + } |
| 75 | + // COBS guarantees a zero-free payload, so every 0x00 on the wire is a frame |
| 76 | + // delimiter — provably one per frame. |
| 77 | + println!( |
| 78 | + "device: {} readings -> {} wire bytes; the only zeros are the {} frame delimiters", |
| 79 | + readings.len(), |
| 80 | + wire.len(), |
| 81 | + readings.len(), |
| 82 | + ); |
| 83 | + |
| 84 | + // --- Inject line noise: corrupt reading #2's leading COBS code byte. --- |
| 85 | + // A bogus code byte makes the frame claim more data than it holds, so it fails |
| 86 | + // to decode — a stand-in for any bit-flip a noisy UART might introduce. |
| 87 | + wire[frame_starts[1]] = 0xFF; |
| 88 | + println!("noise: flipped a byte inside reading #2's frame\n"); |
| 89 | + |
| 90 | + // --- Host side: reassemble across misaligned UART reads and protobuf-decode. --- |
| 91 | + let mut rx = FrameDecoder::new().max_frame_len(256); |
| 92 | + let mut received = 0usize; |
| 93 | + // A UART hands you whatever bytes have arrived, never aligned to frames. |
| 94 | + for chunk in wire.chunks(5) { |
| 95 | + rx.push(chunk, |frame| match frame { |
| 96 | + Ok(bytes) => match SensorReading::decode(&*bytes) { |
| 97 | + Ok(msg) => { |
| 98 | + received += 1; |
| 99 | + println!( |
| 100 | + "host <- node {:>2}: {:+6.2} C, up {:>6} ms, \"{}\"", |
| 101 | + msg.node_id, msg.temperature_c, msg.uptime_ms, msg.label, |
| 102 | + ); |
| 103 | + } |
| 104 | + Err(err) => println!("host <- valid COBS frame but bad protobuf: {err}"), |
| 105 | + }, |
| 106 | + Err(err) => println!("host <- dropped a corrupt frame ({err}); resyncing on next 0x00"), |
| 107 | + }); |
| 108 | + } |
| 109 | + |
| 110 | + println!( |
| 111 | + "\nreceived {received}/{} readings: the corrupted frame was skipped and the \ |
| 112 | + stream resynced on the next delimiter.", |
| 113 | + readings.len(), |
| 114 | + ); |
| 115 | + assert_eq!(received, readings.len() - 1); |
| 116 | +} |
0 commit comments