https://github.com/firechip/cobs_codec
Consistent Overhead Byte Stuffing (COBS) and COBS/R codecs for zero-free, low-overhead framing of serial, UART and packet byte streams in Dart & Flutter.
https://github.com/firechip/cobs_codec
byte-stuffing cobs cobs-r dart embedded encoding flutter framing serial serialization
Last synced: 1 day ago
JSON representation
Consistent Overhead Byte Stuffing (COBS) and COBS/R codecs for zero-free, low-overhead framing of serial, UART and packet byte streams in Dart & Flutter.
- Host: GitHub
- URL: https://github.com/firechip/cobs_codec
- Owner: firechip
- License: mit
- Created: 2026-07-03T19:00:52.000Z (3 days ago)
- Default Branch: main
- Last Pushed: 2026-07-04T11:53:01.000Z (2 days ago)
- Last Synced: 2026-07-04T12:06:51.284Z (2 days ago)
- Topics: byte-stuffing, cobs, cobs-r, dart, embedded, encoding, flutter, framing, serial, serialization
- Language: Dart
- Size: 91.8 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# cobs_codec
[](https://github.com/firechip/cobs_codec/actions/workflows/ci.yml)
[](https://pub.dev/packages/cobs_codec)
[](https://pub.dev/packages/cobs_codec/score)
[](LICENSE)
Fast, dependency-free **Consistent Overhead Byte Stuffing (COBS)** and **COBS/R**
codecs for Dart and Flutter, with first-class `dart:convert` integration and
stream framing for serial links.
COBS encodes an arbitrary byte sequence into one that contains **no zero
(`0x00`) bytes**, at a small and *predictable* cost β at most **one extra byte
per 254 bytes**, plus one. That lets a single `0x00` reliably delimit packets on
a byte stream (serial/UART, USB-CDC, TCP, BLE, β¦), so a receiver can always
resynchronise on the next `0x00` even after a corrupt packet. Unlike escape-based
schemes such as PPP byte stuffing, COBS never doubles a packet's size.
## Features
- β‘ **Basic COBS** and **COBS/R (Reduced)** β pick the standard scheme, or COBS/R
to shave the trailing overhead byte off small messages.
- π§© **`dart:convert` native** β `cobs` and `cobsr` are `Codec`s, so they
`fuse`, `transform` streams, and compose like `json`/`utf8`/`base64`.
- π **Stream framing built in** β turn a raw serial byte stream into a stream of
decoded packets with `CobsFrameDecoder`; chunk boundaries don't have to align
with frames.
- π© **Configurable delimiter (sentinel)** β encode and decode COBS *and* COBS/R
so the output avoids any chosen byte, not just `0x00`, letting that byte delimit
frames instead.
- π·οΈ **Sentinel-aware framing** β the framing helpers and the stream decoder can
split on the chosen `sentinel` rather than `0x00`.
- β»οΈ **In-place decode** β decode basic COBS inside the same buffer with no second
allocation; COBS never expands on decode.
- π― **Zero dependencies, all platforms** β pure Dart (`dart:typed_data`), works
on mobile, desktop, web, server and CLI. Uses `Uint8List` throughout.
- π **Predictable sizing** β `maxEncodedLength` / `encodingOverhead` for buffer
pre-allocation.
- β
**Reference-verified** β tested against the golden vectors from the original
COBS and COBS/R implementations.
## Install
```console
dart pub add cobs_codec
```
```yaml
dependencies:
cobs_codec: ^1.1.0
```
## Usage
### Encode and decode
```dart
import 'package:cobs_codec/cobs_codec.dart';
void main() {
final data = [0x11, 0x22, 0x00, 0x33];
final encoded = cobs.encode(data); // [0x03, 0x11, 0x22, 0x02, 0x33] β no 0x00
final decoded = cobs.decode(encoded); // [0x11, 0x22, 0x00, 0x33]
}
```
`cobs.encode` returns a `Uint8List` and never fails β any input is encodable.
`cobs.decode` throws a `CobsDecodeException` (a `FormatException`) if the input
is not valid COBS.
### COBS/R β save a byte
COBS always adds exactly one byte to messages of 254 bytes or fewer. COBS/R
opportunistically avoids that byte when the final data byte allows it:
```dart
cobs.encode([0x31, 0x32, 0x33, 0x34, 0x35]); // [0x06, 0x31, 0x32, 0x33, 0x34, 0x35]
cobsr.encode([0x31, 0x32, 0x33, 0x34, 0x35]); // [0x35, 0x31, 0x32, 0x33, 0x34] (same size!)
```
Both round-trip losslessly; just decode with the matching codec.
### Framing a packet stream
COBS output has no `0x00`, so append one to delimit frames:
```dart
final frame = cobsFrame([0x11, 0x00, 0x22]); // [0x02, 0x11, 0x02, 0x22, 0x00]
final packets = cobsUnframe(buffer); // List, one per 0x00-delimited frame
```
### Reading packets from a serial stream
`CobsFrameDecoder` is a `StreamTransformer` that buffers bytes across arbitrarily
chunked reads and emits one decoded packet per completed frame β exactly what you
want on a UART:
```dart
import 'package:cobs_codec/cobs_codec.dart';
// `serialPort` is any Stream> of incoming bytes.
serialPort
.transform(CobsFrameDecoder(
// Keep receiving even if a frame is corrupted on a noisy link.
onInvalidFrame: (error, rawFrame) => print('dropped bad frame: $error'),
// Bound memory if a peer never sends the 0x00 delimiter.
maxFrameLength: 4096,
))
.listen((packet) => handlePacket(packet));
// Sending: encode + delimit each outgoing packet.
outgoingPackets
.transform(const CobsFrameEncoder())
.listen(serialPort.add);
```
### A custom delimiter byte (sentinel)
Need a delimiter other than `0x00` β say a byte your protocol reserves for
framing? The `β¦WithSentinel` variants make the encoding avoid an arbitrary
`sentinel` byte instead (they XOR the finished encoding with it), so that byte can
delimit frames:
```dart
final data = [0x11, 0x22, 0x00, 0x33];
final encoded = cobsEncodeWithSentinel(data, 0xAA); // [0xA9, 0xBB, 0x88, 0xA8, 0x99] β no 0xAA
final decoded = cobsDecodeWithSentinel(encoded, 0xAA); // [0x11, 0x22, 0x00, 0x33]
// COBS/R has the same pair.
cobsrEncodeWithSentinel(data, 0xAA); // [0xA9, 0xBB, 0x88, 0x99]
```
`sentinel == 0` is byte-for-byte identical to the plain codec, and decoding never
mutates its input.
### Decoding in place
Basic COBS never expands on decode, so it can be decoded within the same buffer β
no second allocation. `cobsDecodeInPlace` overwrites the buffer with the decoded
bytes and returns their length:
```dart
final buffer = Uint8List.fromList(cobs.encode([0x11, 0x22, 0x00, 0x33]));
// buffer == [0x03, 0x11, 0x22, 0x02, 0x33]
final n = cobsDecodeInPlace(buffer); // 4
final decoded = Uint8List.sublistView(buffer, 0, n); // [0x11, 0x22, 0x00, 0x33]
```
`cobsDecodeInPlaceWithSentinel(buffer, sentinel)` does the same for a custom
delimiter. (COBS/R can expand on decode, so it has no in-place form.)
### Framing on a custom sentinel
`cobsFrame` / `cobsUnframe` and both stream transformers take the same optional
`sentinel`, so a whole link can be delimited by a byte other than `0x00`:
```dart
final frame = cobsFrame([0x11, 0x00, 0x22], sentinel: 0xAA); // [0xA8, 0xBB, 0xA8, 0x88, 0xAA]
final packets = cobsUnframe(frame, sentinel: 0xAA); // [[0x11, 0x00, 0x22]]
// The stream decoder frames on the same byte:
serialPort.transform(const CobsFrameDecoder(sentinel: 0xAA));
```
### Composing with other codecs
Because they are `Codec`s, you can `fuse` COBS with anything:
```dart
// Encode a Dart object to JSON, to UTF-8 bytes, then COBS-frame it.
final pipeline = json.fuse(utf8).fuse(cobs);
final wire = pipeline.encode({'id': 7, 'ok': true});
final obj = pipeline.decode(wire);
```
### Sizing buffers
```dart
encodingOverhead(0); // 1
encodingOverhead(254); // 1
encodingOverhead(255); // 2
maxEncodedLength(1000); // 1004
```
## How much overhead?
| Input length *n* | Max encoded length | Overhead |
| --------------------- | ------------------ | ------------- |
| 0 (empty) | 1 | +1 byte |
| 1 β 254 | *n* + 1 | +1 byte |
| 255 β 508 | *n* + 2 | +2 bytes |
| *n* | *n* + β*n* / 254β | β€ ~0.4% |
The overhead is *data-independent*: worst case and average case are almost the
same. Compare PPP/SLIP escape stuffing, whose worst case **doubles** the packet.
## API overview
| Symbol | Description |
| ------ | ----------- |
| `cobs` / `cobsr` | Shared `Codec` instances (basic COBS and COBS/R). |
| `cobsEncode` / `cobsDecode` | Direct basic-COBS functions. |
| `cobsrEncode` / `cobsrDecode` | Direct COBS/R functions. |
| `cobsEncodeWithSentinel` / `cobsDecodeWithSentinel` | Basic COBS against an arbitrary delimiter byte. |
| `cobsrEncodeWithSentinel` / `cobsrDecodeWithSentinel` | COBS/R against an arbitrary delimiter byte. |
| `cobsDecodeInPlace` / `cobsDecodeInPlaceWithSentinel` | Decode basic COBS within the buffer; returns the length. |
| `CobsCodec`, `CobsEncoder`, `CobsDecoder` | `dart:convert` classes for basic COBS. |
| `CobsrCodec`, `CobsrEncoder`, `CobsrDecoder` | `dart:convert` classes for COBS/R. |
| `cobsFrame` / `cobsUnframe` | Add / split the frame delimiter (`0x00` or a custom `sentinel`). |
| `CobsFrameEncoder` / `CobsFrameDecoder` | Stream transformers for framed links (optional `sentinel`). |
| `cobsDelimiter` | The frame delimiter byte (`0x00`). |
| `encodingOverhead` / `maxEncodedLength` | Size bounds. |
| `cobsMaxBlockLength` | Max data bytes per COBS block (`254`). |
| `CobsDecodeException` | Thrown on invalid encoded input. |
> **Note on decoding.** `decode` expects a *single* encoded packet with **no**
> surrounding `0x00` delimiter β split a delimited stream into frames first (with
> `cobsUnframe` or `CobsFrameDecoder`). This matches the reference COBS
> implementations, where framing is the application's responsibility.
## Benchmarks
Single-threaded throughput on a 1 KiB payload
(`dart run benchmark/cobs_benchmark.dart`), Dart 3.x on an AMD Ryzen 7 3800XT
under WSL2 β indicative, not a controlled benchmark:
| Operation | Throughput |
| --------- | ---------- |
| `cobsEncode` | ~580 MB/s |
| `cobsDecode` | ~570 MB/s |
| `cobsrEncode` | ~610 MB/s |
## Background
COBS is described in:
> Stuart Cheshire and Mary Baker, "Consistent Overhead Byte Stuffing,"
> *IEEE/ACM Transactions on Networking*, Vol. 7, No. 2, April 1999.
**COBS/R** ("Reduced") was devised by **Craig McQueen**, whose C and Python
reference implementations were used to validate this package's test vectors. The
COBS/ZPE and COBS/ZRE variants are not implemented.
## Contributing
Contributions are welcome β see [CONTRIBUTING.md](CONTRIBUTING.md).
## License
MIT Β© 2026 Alexander Salas Bastidas ([Firechip](https://firechip.dev)). See
[LICENSE](LICENSE).