|
| 1 | +# Integrating with async & streaming frameworks |
| 2 | + |
| 3 | +`cobs_codec_rs` deliberately keeps its core `#![no_std]` and dependency-free: it |
| 4 | +encodes, decodes, and frames over plain `&[u8]`, and nothing else. Adapters to a |
| 5 | +specific async runtime or I/O framework live in **your** project, built on the |
| 6 | +crate's public API — so the core never drags `tokio`, `bytes`, or an embedded |
| 7 | +HAL into everyone's dependency tree. |
| 8 | + |
| 9 | +The recipes below are copy-paste starting points, **not** part of the published |
| 10 | +crate. Each is verified against the crate's real API. |
| 11 | + |
| 12 | +## Tokio — a `tokio_util::codec` for `Framed` streams |
| 13 | + |
| 14 | +A complete `Encoder` + `Decoder` that frames each packet as COBS plus a `0x00` |
| 15 | +delimiter, so it drops straight into `Framed` / `FramedRead` / `FramedWrite` |
| 16 | +over any `AsyncRead` / `AsyncWrite` (a TCP socket, or a serial port via |
| 17 | +`tokio-serial`, …). |
| 18 | + |
| 19 | +In *your* `Cargo.toml`: |
| 20 | + |
| 21 | +```toml |
| 22 | +[dependencies] |
| 23 | +cobs_codec_rs = "1" |
| 24 | +tokio-util = { version = "0.7", features = ["codec"] } |
| 25 | +bytes = "1" |
| 26 | +``` |
| 27 | + |
| 28 | +```rust |
| 29 | +use bytes::{Buf, BytesMut}; |
| 30 | +use cobs_codec_rs::{cobs, max_encoded_len, DELIMITER}; |
| 31 | +use tokio_util::codec::{Decoder, Encoder}; |
| 32 | + |
| 33 | +/// Frames each packet with COBS and a trailing `0x00` delimiter. |
| 34 | +#[derive(Default)] |
| 35 | +pub struct CobsFrameCodec; |
| 36 | + |
| 37 | +impl Encoder<&[u8]> for CobsFrameCodec { |
| 38 | + type Error = std::io::Error; |
| 39 | + |
| 40 | + fn encode(&mut self, item: &[u8], dst: &mut BytesMut) -> Result<(), Self::Error> { |
| 41 | + let start = dst.len(); |
| 42 | + // Worst-case COBS output, plus one byte for the frame delimiter. |
| 43 | + dst.resize(start + max_encoded_len(item.len()) + 1, 0); |
| 44 | + let n = cobs::encode(item, &mut dst[start..]); |
| 45 | + dst[start + n] = DELIMITER; |
| 46 | + dst.truncate(start + n + 1); |
| 47 | + Ok(()) |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +impl Decoder for CobsFrameCodec { |
| 52 | + type Item = Vec<u8>; |
| 53 | + type Error = std::io::Error; |
| 54 | + |
| 55 | + fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { |
| 56 | + // A frame is everything up to the next delimiter; wait if it is absent. |
| 57 | + let Some(pos) = src.iter().position(|&b| b == DELIMITER) else { |
| 58 | + return Ok(None); |
| 59 | + }; |
| 60 | + let frame = src.split_to(pos); |
| 61 | + src.advance(1); // discard the delimiter |
| 62 | + cobs::decode_to_vec(&frame) |
| 63 | + .map(Some) |
| 64 | + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +Then: |
| 70 | + |
| 71 | +```rust |
| 72 | +use futures::{SinkExt, StreamExt}; |
| 73 | +use tokio_util::codec::{FramedRead, FramedWrite}; |
| 74 | + |
| 75 | +// write side |
| 76 | +let mut w = FramedWrite::new(writer, CobsFrameCodec); |
| 77 | +w.send(&b"\x11\x00\x22"[..]).await?; |
| 78 | + |
| 79 | +// read side — yields each decoded packet |
| 80 | +let mut r = FramedRead::new(reader, CobsFrameCodec); |
| 81 | +while let Some(packet) = r.next().await { |
| 82 | + let packet = packet?; |
| 83 | + // ... |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +## Embedded — `embedded-io` / `embedded-io-async` |
| 88 | + |
| 89 | +The same shape works `no_std`. Scan an `embedded_io::Read` for the `0x00` |
| 90 | +delimiter into a fixed `[u8; N]`, then call `cobs::decode` into a decoded buffer |
| 91 | +— or `cobs::decode_in_place` to decode within the same buffer. Because the core |
| 92 | +needs no allocator, the whole path is allocation-free. The crate's |
| 93 | +`framing::FrameDecoder` already does the incremental delimiter scanning if you |
| 94 | +would rather reuse it than hand-roll the loop. |
| 95 | + |
| 96 | +## What stays in the core |
| 97 | + |
| 98 | +Everything you build on: `cobs` / `cobsr` encode/decode (with sentinel and |
| 99 | +in-place variants), `framing` for `0x00`-delimited streams, and the size helpers |
| 100 | +`max_encoded_len` / `encoding_overhead`. The adapters above are just the seam |
| 101 | +where your chosen framework meets that API. |
0 commit comments