@@ -23,8 +23,29 @@ const int cobsDelimiter = 0x00;
2323Uint8List _asBytes (List <int > bytes) =>
2424 bytes is Uint8List ? bytes : Uint8List .fromList (bytes);
2525
26- /// Encodes [packet] with [codec] (basic [cobs] by default) and appends the
27- /// [cobsDelimiter] , producing a self-delimiting frame ready to transmit.
26+ /// Returns the frame `bytes[start..end)` XORed by [sentinel] , ready to hand to a
27+ /// codec's plain decoder.
28+ ///
29+ /// For a non-zero [sentinel] a fresh copy is made so the caller's [bytes] are
30+ /// never mutated; for `0` the slice is viewed without copying, preserving the
31+ /// zero-copy behaviour of the plain delimiter path.
32+ Uint8List _deSentinel (Uint8List bytes, int start, int end, int sentinel) {
33+ if (sentinel == 0 ) return Uint8List .sublistView (bytes, start, end);
34+ final frame = Uint8List (end - start);
35+ for (var i = 0 ; i < frame.length; i++ ) {
36+ frame[i] = bytes[start + i] ^ sentinel;
37+ }
38+ return frame;
39+ }
40+
41+ /// Encodes [packet] with [codec] (basic [cobs] by default) and appends the frame
42+ /// delimiter, producing a self-delimiting frame ready to transmit.
43+ ///
44+ /// By default frames are delimited by the [cobsDelimiter] (`0x00` ). Pass a
45+ /// non-zero [sentinel] to delimit on that byte instead: the encoding is made to
46+ /// avoid [sentinel] (by XORing the codec's output with it) and [sentinel] is
47+ /// appended as the delimiter. Decode such a frame with a matching [sentinel] in
48+ /// [cobsUnframe] or [CobsFrameDecoder] .
2849///
2950/// ```dart
3051/// final frame = cobsFrame([0x11, 0x00, 0x22]);
@@ -33,17 +54,29 @@ Uint8List _asBytes(List<int> bytes) =>
3354Uint8List cobsFrame (
3455 List <int > packet, {
3556 Codec <List <int >, List <int >> codec = cobs,
57+ int sentinel = cobsDelimiter,
3658}) {
3759 final encoded = codec.encode (packet);
3860 final out = Uint8List (encoded.length + 1 );
3961 out.setRange (0 , encoded.length, encoded);
40- // out[encoded.length] is already 0x00 (the delimiter).
62+ final s = sentinel & 0xFF ;
63+ if (s != 0 ) {
64+ for (var i = 0 ; i < encoded.length; i++ ) {
65+ out[i] ^ = s;
66+ }
67+ }
68+ out[encoded.length] = s; // the delimiter (already 0 when s == 0)
4169 return out;
4270}
4371
44- /// Splits [data] on the [cobsDelimiter] and decodes each frame with [codec] ,
72+ /// Splits [data] on the frame delimiter and decodes each frame with [codec] ,
4573/// returning the list of recovered packets.
4674///
75+ /// By default frames are delimited by the [cobsDelimiter] (`0x00` ). Pass the
76+ /// non-zero [sentinel] used to frame the data to split on that byte instead and
77+ /// undo its XOR before decoding; the frame slice is copied before it is XORed, so
78+ /// [data] is never mutated.
79+ ///
4780/// Any trailing bytes after the final delimiter are treated as an incomplete
4881/// frame and ignored. When [skipEmpty] is true (the default), empty frames
4982/// (produced by consecutive or leading delimiters) are skipped rather than
@@ -55,17 +88,18 @@ List<Uint8List> cobsUnframe(
5588 List <int > data, {
5689 Codec <List <int >, List <int >> codec = cobs,
5790 bool skipEmpty = true ,
91+ int sentinel = cobsDelimiter,
5892}) {
5993 final frames = < Uint8List > [];
6094 final bytes = _asBytes (data);
95+ final s = sentinel & 0xFF ;
6196 var start = 0 ;
6297 for (var i = 0 ; i < bytes.length; i++ ) {
63- if (bytes[i] == cobsDelimiter ) {
98+ if (bytes[i] == s ) {
6499 if (i == start) {
65100 if (! skipEmpty) frames.add (Uint8List (0 ));
66101 } else {
67- frames.add (
68- _asBytes (codec.decode (Uint8List .sublistView (bytes, start, i))));
102+ frames.add (_asBytes (codec.decode (_deSentinel (bytes, start, i, s))));
69103 }
70104 start = i + 1 ;
71105 }
@@ -80,16 +114,21 @@ List<Uint8List> cobsUnframe(
80114/// outgoingPackets.transform(const CobsFrameEncoder()).pipe(serialSink);
81115/// ```
82116class CobsFrameEncoder extends StreamTransformerBase <List <int >, Uint8List > {
83- /// Creates a frame encoder using [codec] (basic [cobs] by default).
84- const CobsFrameEncoder ({this .codec = cobs});
117+ /// Creates a frame encoder using [codec] (basic [cobs] by default), delimiting
118+ /// each frame with [sentinel] (the `0x00` [cobsDelimiter] by default).
119+ const CobsFrameEncoder ({this .codec = cobs, this .sentinel = cobsDelimiter});
85120
86121 /// The codec used to encode each packet.
87122 final Codec <List <int >, List <int >> codec;
88123
124+ /// The byte value used to delimit frames on the wire (and that the encoding is
125+ /// made to avoid). Defaults to the `0x00` [cobsDelimiter] .
126+ final int sentinel;
127+
89128 @override
90129 Stream <Uint8List > bind (Stream <List <int >> stream) async * {
91130 await for (final packet in stream) {
92- yield cobsFrame (packet, codec: codec);
131+ yield cobsFrame (packet, codec: codec, sentinel : sentinel );
93132 }
94133 }
95134}
@@ -125,11 +164,17 @@ class CobsFrameDecoder extends StreamTransformerBase<List<int>, Uint8List> {
125164 this .skipEmpty = true ,
126165 this .maxFrameLength,
127166 this .onInvalidFrame,
167+ this .sentinel = cobsDelimiter,
128168 });
129169
130170 /// The codec used to decode each frame.
131171 final Codec <List <int >, List <int >> codec;
132172
173+ /// The byte value that delimits frames on the wire (and that the encoding is
174+ /// made to avoid). The stream is split on this byte and each frame is XORed
175+ /// back with it before decoding. Defaults to the `0x00` [cobsDelimiter] .
176+ final int sentinel;
177+
133178 /// Whether to skip empty frames (from consecutive or leading delimiters)
134179 /// rather than emit empty packets.
135180 final bool skipEmpty;
@@ -160,6 +205,7 @@ class CobsFrameDecoder extends StreamTransformerBase<List<int>, Uint8List> {
160205 // completes a frame — so a delimiter-less run would otherwise ignore
161206 // backpressure and make the subscription impossible to cancel.
162207 final buffer = BytesBuilder (copy: false );
208+ final delimiter = sentinel & 0xFF ;
163209 late final StreamController <Uint8List > controller;
164210 StreamSubscription <List <int >>? subscription;
165211
@@ -177,7 +223,7 @@ class CobsFrameDecoder extends StreamTransformerBase<List<int>, Uint8List> {
177223 final bytes = _asBytes (chunk);
178224 var start = 0 ;
179225 for (var i = 0 ; i < bytes.length; i++ ) {
180- if (bytes[i] != cobsDelimiter ) continue ;
226+ if (bytes[i] != delimiter ) continue ;
181227 // The delimiter completes a frame: buffered bytes + this chunk up to
182228 // (but not including) the delimiter. This sublist view is consumed
183229 // synchronously by takeBytes/decode below, so it need not be copied.
@@ -188,6 +234,14 @@ class CobsFrameDecoder extends StreamTransformerBase<List<int>, Uint8List> {
188234 if (! skipEmpty) controller.add (Uint8List (0 ));
189235 continue ;
190236 }
237+ // `frame` is owned by this decoder (from takeBytes), so undoing the
238+ // sentinel XOR in place is safe; when the delimiter is 0x00 this is a
239+ // no-op and the plain-COBS path is unchanged.
240+ if (delimiter != 0 ) {
241+ for (var j = 0 ; j < frame.length; j++ ) {
242+ frame[j] ^ = delimiter;
243+ }
244+ }
191245 try {
192246 controller.add (_asBytes (codec.decode (frame)));
193247 } on CobsDecodeException catch (error, stackTrace) {
0 commit comments