Skip to content

Commit ba29437

Browse files
committed
docs: add a Protobuf + COBS framing example
Runnable device->host protobuf-over-COBS demo (prost dev-dep); shows resync.
1 parent 7d5f26d commit ba29437

4 files changed

Lines changed: 218 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 88 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ alloc = []
2929
# Enables `std` (implies `alloc`). Reserved for future std-only conveniences.
3030
std = ["alloc"]
3131

32+
[dev-dependencies]
33+
# Real Protocol Buffers, used only by the `protobuf_cobs` example. Dev-only, so
34+
# the published crate stays dependency-free for library users.
35+
prost = "0.14"
36+
3237
[lints.rust]
3338
missing_docs = "warn"
3439
unsafe_code = "forbid"

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ rx.push(&chunk, |frame| match frame {
136136
});
137137
```
138138

139+
### Framing Protocol Buffers over serial
140+
141+
COBS is the standard way to frame **Protobuf** on a UART/RS-485 link: protobuf
142+
serializes a message but doesn't delimit it, and COBS supplies the missing
143+
`0x00`-delimited framing — with instant resync after line noise, unlike
144+
length-prefixing. See [`examples/protobuf_cobs.rs`](examples/protobuf_cobs.rs)
145+
for a runnable device→host demo (`cargo run --example protobuf_cobs`) that
146+
survives a corrupted frame.
147+
139148
## Overhead
140149

141150
COBS overhead is *data-independent*. Encoding an `n`-byte packet produces at most

examples/protobuf_cobs.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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

Comments
 (0)