|
| 1 | +# cobs_codec |
| 2 | + |
| 3 | +[](https://pub.dev/packages/cobs_codec) |
| 4 | +[](https://pub.dev/packages/cobs_codec/score) |
| 5 | +[](LICENSE) |
| 6 | + |
| 7 | +Fast, dependency-free **Consistent Overhead Byte Stuffing (COBS)** and **COBS/R** |
| 8 | +codecs for Dart and Flutter, with first-class `dart:convert` integration and |
| 9 | +stream framing for serial links. |
| 10 | + |
| 11 | +COBS encodes an arbitrary byte sequence into one that contains **no zero |
| 12 | +(`0x00`) bytes**, at a small and *predictable* cost — at most **one extra byte |
| 13 | +per 254 bytes**, plus one. That lets a single `0x00` reliably delimit packets on |
| 14 | +a byte stream (serial/UART, USB-CDC, TCP, BLE, …), so a receiver can always |
| 15 | +resynchronise on the next `0x00` even after a corrupt packet. Unlike escape-based |
| 16 | +schemes such as PPP byte stuffing, COBS never doubles a packet's size. |
| 17 | + |
| 18 | +## Features |
| 19 | + |
| 20 | +- ⚡ **Basic COBS** and **COBS/R (Reduced)** — pick the standard scheme, or COBS/R |
| 21 | + to shave the trailing overhead byte off small messages. |
| 22 | +- 🧩 **`dart:convert` native** — `cobs` and `cobsr` are `Codec`s, so they |
| 23 | + `fuse`, `transform` streams, and compose like `json`/`utf8`/`base64`. |
| 24 | +- 🔌 **Stream framing built in** — turn a raw serial byte stream into a stream of |
| 25 | + decoded packets with `CobsFrameDecoder`; chunk boundaries don't have to align |
| 26 | + with frames. |
| 27 | +- 🎯 **Zero dependencies, all platforms** — pure Dart (`dart:typed_data`), works |
| 28 | + on mobile, desktop, web, server and CLI. Uses `Uint8List` throughout. |
| 29 | +- 📏 **Predictable sizing** — `maxEncodedLength` / `encodingOverhead` for buffer |
| 30 | + pre-allocation. |
| 31 | +- ✅ **Reference-verified** — tested against the golden vectors from the original |
| 32 | + COBS and COBS/R implementations. |
| 33 | + |
| 34 | +## Install |
| 35 | + |
| 36 | +```console |
| 37 | +dart pub add cobs_codec |
| 38 | +``` |
| 39 | + |
| 40 | +```yaml |
| 41 | +dependencies: |
| 42 | + cobs_codec: ^1.0.0 |
| 43 | +``` |
| 44 | +
|
| 45 | +## Usage |
| 46 | +
|
| 47 | +### Encode and decode |
| 48 | +
|
| 49 | +```dart |
| 50 | +import 'package:cobs_codec/cobs_codec.dart'; |
| 51 | + |
| 52 | +void main() { |
| 53 | + final data = [0x11, 0x22, 0x00, 0x33]; |
| 54 | + |
| 55 | + final encoded = cobs.encode(data); // [0x03, 0x11, 0x22, 0x02, 0x33] — no 0x00 |
| 56 | + final decoded = cobs.decode(encoded); // [0x11, 0x22, 0x00, 0x33] |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +`cobs.encode` returns a `Uint8List` and never fails — any input is encodable. |
| 61 | +`cobs.decode` throws a `CobsDecodeException` (a `FormatException`) if the input |
| 62 | +is not valid COBS. |
| 63 | + |
| 64 | +### COBS/R — save a byte |
| 65 | + |
| 66 | +COBS always adds exactly one byte to messages of 254 bytes or fewer. COBS/R |
| 67 | +opportunistically avoids that byte when the final data byte allows it: |
| 68 | + |
| 69 | +```dart |
| 70 | +cobs.encode([0x31, 0x32, 0x33, 0x34, 0x35]); // [0x06, 0x31, 0x32, 0x33, 0x34, 0x35] |
| 71 | +cobsr.encode([0x31, 0x32, 0x33, 0x34, 0x35]); // [0x35, 0x31, 0x32, 0x33, 0x34] (same size!) |
| 72 | +``` |
| 73 | + |
| 74 | +Both round-trip losslessly; just decode with the matching codec. |
| 75 | + |
| 76 | +### Framing a packet stream |
| 77 | + |
| 78 | +COBS output has no `0x00`, so append one to delimit frames: |
| 79 | + |
| 80 | +```dart |
| 81 | +final frame = cobsFrame([0x11, 0x00, 0x22]); // [0x02, 0x11, 0x02, 0x22, 0x00] |
| 82 | +
|
| 83 | +final packets = cobsUnframe(buffer); // List<Uint8List>, one per 0x00-delimited frame |
| 84 | +``` |
| 85 | + |
| 86 | +### Reading packets from a serial stream |
| 87 | + |
| 88 | +`CobsFrameDecoder` is a `StreamTransformer` that buffers bytes across arbitrarily |
| 89 | +chunked reads and emits one decoded packet per completed frame — exactly what you |
| 90 | +want on a UART: |
| 91 | + |
| 92 | +```dart |
| 93 | +import 'package:cobs_codec/cobs_codec.dart'; |
| 94 | +
|
| 95 | +// `serialPort` is any Stream<List<int>> of incoming bytes. |
| 96 | +serialPort |
| 97 | + .transform(CobsFrameDecoder( |
| 98 | + // Keep receiving even if a frame is corrupted on a noisy link. |
| 99 | + onInvalidFrame: (error, rawFrame) => print('dropped bad frame: $error'), |
| 100 | + // Bound memory if a peer never sends the 0x00 delimiter. |
| 101 | + maxFrameLength: 4096, |
| 102 | + )) |
| 103 | + .listen((packet) => handlePacket(packet)); |
| 104 | +
|
| 105 | +// Sending: encode + delimit each outgoing packet. |
| 106 | +outgoingPackets |
| 107 | + .transform(const CobsFrameEncoder()) |
| 108 | + .listen(serialPort.add); |
| 109 | +``` |
| 110 | + |
| 111 | +### Composing with other codecs |
| 112 | + |
| 113 | +Because they are `Codec`s, you can `fuse` COBS with anything: |
| 114 | + |
| 115 | +```dart |
| 116 | +// Encode a Dart object to JSON, to UTF-8 bytes, then COBS-frame it. |
| 117 | +final pipeline = json.fuse(utf8).fuse(cobs); |
| 118 | +final wire = pipeline.encode({'id': 7, 'ok': true}); |
| 119 | +final obj = pipeline.decode(wire); |
| 120 | +``` |
| 121 | + |
| 122 | +### Sizing buffers |
| 123 | + |
| 124 | +```dart |
| 125 | +encodingOverhead(0); // 1 |
| 126 | +encodingOverhead(254); // 1 |
| 127 | +encodingOverhead(255); // 2 |
| 128 | +maxEncodedLength(1000); // 1004 |
| 129 | +``` |
| 130 | + |
| 131 | +## How much overhead? |
| 132 | + |
| 133 | +| Input length *n* | Max encoded length | Overhead | |
| 134 | +| --------------------- | ------------------ | ------------- | |
| 135 | +| 0 (empty) | 1 | +1 byte | |
| 136 | +| 1 – 254 | *n* + 1 | +1 byte | |
| 137 | +| 255 – 508 | *n* + 2 | +2 bytes | |
| 138 | +| *n* | *n* + ⌈*n* / 254⌉ | ≤ ~0.4% | |
| 139 | + |
| 140 | +The overhead is *data-independent*: worst case and average case are almost the |
| 141 | +same. Compare PPP/SLIP escape stuffing, whose worst case **doubles** the packet. |
| 142 | + |
| 143 | +## API overview |
| 144 | + |
| 145 | +| Symbol | Description | |
| 146 | +| ------ | ----------- | |
| 147 | +| `cobs` / `cobsr` | Shared `Codec` instances (basic COBS and COBS/R). | |
| 148 | +| `cobsEncode` / `cobsDecode` | Direct basic-COBS functions. | |
| 149 | +| `cobsrEncode` / `cobsrDecode` | Direct COBS/R functions. | |
| 150 | +| `CobsCodec`, `CobsEncoder`, `CobsDecoder` | `dart:convert` classes for basic COBS. | |
| 151 | +| `CobsrCodec`, `CobsrEncoder`, `CobsrDecoder` | `dart:convert` classes for COBS/R. | |
| 152 | +| `cobsFrame` / `cobsUnframe` | Add / split the `0x00` frame delimiter. | |
| 153 | +| `CobsFrameEncoder` / `CobsFrameDecoder` | Stream transformers for framed links. | |
| 154 | +| `cobsDelimiter` | The frame delimiter byte (`0x00`). | |
| 155 | +| `encodingOverhead` / `maxEncodedLength` | Size bounds. | |
| 156 | +| `cobsMaxBlockLength` | Max data bytes per COBS block (`254`). | |
| 157 | +| `CobsDecodeException` | Thrown on invalid encoded input. | |
| 158 | + |
| 159 | +> **Note on decoding.** `decode` expects a *single* encoded packet with **no** |
| 160 | +> surrounding `0x00` delimiter — split a delimited stream into frames first (with |
| 161 | +> `cobsUnframe` or `CobsFrameDecoder`). This matches the reference COBS |
| 162 | +> implementations, where framing is the application's responsibility. |
| 163 | +
|
| 164 | +## Background |
| 165 | + |
| 166 | +COBS is described in: |
| 167 | + |
| 168 | +> Stuart Cheshire and Mary Baker, "Consistent Overhead Byte Stuffing," |
| 169 | +> *IEEE/ACM Transactions on Networking*, Vol. 7, No. 2, April 1999. |
| 170 | +
|
| 171 | +**COBS/R** ("Reduced") was devised by **Craig McQueen**, whose C and Python |
| 172 | +reference implementations were used to validate this package's test vectors. The |
| 173 | +COBS/ZPE and COBS/ZRE variants are not implemented. |
| 174 | + |
| 175 | +## Contributing |
| 176 | + |
| 177 | +Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). |
| 178 | + |
| 179 | +## License |
| 180 | + |
| 181 | +MIT © 2026 Alexander Salas Bastidas ([Firechip](https://firechip.dev)). See |
| 182 | +[LICENSE](LICENSE). |
0 commit comments