forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathring_protocol_test.dart
More file actions
344 lines (307 loc) · 13.1 KB
/
Copy pathring_protocol_test.dart
File metadata and controls
344 lines (307 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:omi/services/devices/ring_protocol.dart';
void main() {
group('RingProtocol.parseStatus', () {
test('decodes 16-byte status with four LE u32 fields', () {
// used=1234, unread=42, free=99999, rtcValid=1
final bytes = ByteData(16)
..setUint32(0, 1234, Endian.little)
..setUint32(4, 42, Endian.little)
..setUint32(8, 99999, Endian.little)
..setUint32(12, 1, Endian.little);
final status = RingProtocol.parseStatus(bytes.buffer.asUint8List());
expect(status, isNotNull);
expect(status!.usedBytes, 1234);
expect(status.unreadPackets, 42);
expect(status.freeBytes, 99999);
expect(status.rtcValid, 1);
expect(status.isRtcValid, isTrue);
});
test('rtcValid=0 surfaces as isRtcValid=false', () {
final bytes = ByteData(16)
..setUint32(0, 0, Endian.little)
..setUint32(4, 0, Endian.little)
..setUint32(8, 0, Endian.little)
..setUint32(12, 0, Endian.little);
final status = RingProtocol.parseStatus(bytes.buffer.asUint8List())!;
expect(status.isRtcValid, isFalse);
});
test('returns null for short payload', () {
expect(RingProtocol.parseStatus([0, 1, 2, 3]), isNull);
expect(RingProtocol.parseStatus(<int>[]), isNull);
});
});
group('RingProtocol.parseInfoNotification', () {
test('decodes a full 31-byte NOTIFY_INFO frame', () {
// [0x02][read:u64 BE][write:u64 BE][cap:u32 BE][dropped:u64 BE][pkt_size:u16 BE]
final bd = ByteData(31)
..setUint8(0, 0x02)
..setUint64(1, 1000, Endian.big)
..setUint64(9, 1500, Endian.big)
..setUint32(17, 4096, Endian.big)
..setUint64(21, 7, Endian.big)
..setUint16(29, 444, Endian.big);
final info = RingProtocol.parseInfoNotification(bd.buffer.asUint8List());
expect(info, isNotNull);
expect(info!.readSeq, 1000);
expect(info.writeSeq, 1500);
expect(info.capacityPackets, 4096);
expect(info.droppedPackets, 7);
expect(info.packetSize, 444);
expect(info.unreadPackets, 500);
});
test('returns null when the leading opcode is not 0x02', () {
final bd = ByteData(31)..setUint8(0, 0x03);
expect(RingProtocol.parseInfoNotification(bd.buffer.asUint8List()), isNull);
});
test('returns null for truncated payload', () {
final bd = ByteData(20)..setUint8(0, 0x02);
expect(RingProtocol.parseInfoNotification(bd.buffer.asUint8List()), isNull);
});
});
group('RingProtocol.parseDoneNotification', () {
test('decodes status + next_seq', () {
final bd = ByteData(10)
..setUint8(0, 0x04)
..setUint8(1, 0)
..setUint64(2, 12345, Endian.big);
final done = RingProtocol.parseDoneNotification(bd.buffer.asUint8List())!;
expect(done.status, 0);
expect(done.nextSeq, 12345);
expect(done.isOk, isTrue);
});
test('non-zero status surfaces as isOk=false', () {
final bd = ByteData(10)
..setUint8(0, 0x04)
..setUint8(1, 6)
..setUint64(2, 0, Endian.big);
final done = RingProtocol.parseDoneNotification(bd.buffer.asUint8List())!;
expect(done.isOk, isFalse);
});
test('returns null for truncated payload', () {
expect(RingProtocol.parseDoneNotification([0x04, 0]), isNull);
});
});
group('RingProtocol.parseReadBeginNotification', () {
test('decodes start_seq + count', () {
final bd = ByteData(13)
..setUint8(0, 0x05)
..setUint64(1, 999, Endian.big)
..setUint32(9, 50, Endian.big);
final begin = RingProtocol.parseReadBeginNotification(bd.buffer.asUint8List())!;
expect(begin.transferStartSeq, 999);
expect(begin.packetCount, 50);
});
test('returns null for truncated payload', () {
expect(RingProtocol.parseReadBeginNotification([0x05, 0, 0, 0, 0, 0, 0]), isNull);
});
});
group('RingProtocol.encodeReadCommand', () {
test('without count produces 9 bytes: [0x11][seq:u64 BE]', () {
final bytes = RingProtocol.encodeReadCommand(0x0123456789ABCDEF);
expect(bytes.length, 9);
expect(bytes[0], 0x11);
// u64 BE: 01 23 45 67 89 AB CD EF
expect(bytes.sublist(1).toList(), [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
});
test('with positive count produces 13 bytes: [0x11][seq:u64 BE][count:u32 BE]', () {
final bytes = RingProtocol.encodeReadCommand(1, packetCount: 100);
expect(bytes.length, 13);
expect(bytes[0], 0x11);
// count u32 BE 100 = 00 00 00 64
expect(bytes.sublist(9).toList(), [0x00, 0x00, 0x00, 0x64]);
});
test('count of 0 omits the count field (stream all)', () {
final bytes = RingProtocol.encodeReadCommand(0, packetCount: 0);
expect(bytes.length, 9);
});
test('null count omits the count field (stream all)', () {
final bytes = RingProtocol.encodeReadCommand(0);
expect(bytes.length, 9);
});
});
group('RingProtocol.encodeAdvanceCommand', () {
test('produces 9 bytes: [0x12][seq:u64 BE]', () {
final bytes = RingProtocol.encodeAdvanceCommand(0xCAFEBABE);
expect(bytes.length, 9);
expect(bytes[0], 0x12);
// 0xCAFEBABE as u64 BE = 00 00 00 00 CA FE BA BE
expect(bytes.sublist(1).toList(), [0x00, 0x00, 0x00, 0x00, 0xCA, 0xFE, 0xBA, 0xBE]);
});
});
group('RingProtocol.readRecordTimestamp', () {
test('reads 4-byte big-endian timestamp prefix', () {
// 0x6824B5C0 = epoch 2026-05-13 21:47:12 UTC
const ts = 0x6824B5C0;
final record = [0x68, 0x24, 0xB5, 0xC0, ...List.filled(440, 0)];
expect(RingProtocol.readRecordTimestamp(record), ts);
});
test('reads 0 timestamp (rtc-invalid sentinel)', () {
final record = [0, 0, 0, 0, ...List.filled(440, 0)];
expect(RingProtocol.readRecordTimestamp(record), 0);
});
});
group('RingProtocol.parseAudioPayload', () {
test('parses a single packed frame', () {
// [size=3][0xAA, 0xBB, 0xCC] then padding zeros
final audio = <int>[3, 0xAA, 0xBB, 0xCC, ...List.filled(436, 0)];
final frames = RingProtocol.parseAudioPayload(audio);
expect(frames.length, 1);
expect(frames[0], [0xAA, 0xBB, 0xCC]);
});
test('parses multiple packed frames', () {
// Two 80-byte frames (typical opus). [80][80B][80][80B] then padding.
final f1 = List<int>.generate(80, (i) => 0xB0 + (i & 0x0F));
final f2 = List<int>.generate(80, (i) => 0xC0 + (i & 0x0F));
final audio = <int>[80, ...f1, 80, ...f2];
audio.addAll(List.filled(440 - audio.length, 0));
final frames = RingProtocol.parseAudioPayload(audio);
expect(frames.length, 2);
expect(frames[0], f1);
expect(frames[1], f2);
});
test('skips zero-byte padding markers between frames', () {
// [size=2][0x01,0x02][0][size=2][0x03,0x04]
final audio = <int>[2, 0x01, 0x02, 0, 2, 0x03, 0x04, ...List.filled(440 - 7, 0)];
final frames = RingProtocol.parseAudioPayload(audio);
expect(frames.length, 2);
expect(frames[0], [0x01, 0x02]);
expect(frames[1], [0x03, 0x04]);
});
test('stops when declared frame size would overrun the buffer', () {
// [size=2][0x01,0x02][size=100, but only 2 bytes follow] — second frame
// is truncated: 100 bytes won't fit, so the parser must drop it and stop.
final audio = <int>[2, 0x01, 0x02, 100, 0xAA, 0xBB];
final frames = RingProtocol.parseAudioPayload(audio);
expect(frames.length, 1);
expect(frames[0], [0x01, 0x02]);
});
test('returns empty for all-zero payload', () {
final frames = RingProtocol.parseAudioPayload(List.filled(440, 0));
expect(frames, isEmpty);
});
test('drops a frame that would end exactly at the buffer boundary (firmware overflow guard)', () {
// [size=2][0xAA, 0xBB] — the frame would end exactly at audio.length.
// The firmware (transport.c:write_to_storage) never writes a frame ending
// exactly at the last byte: a size byte at the boundary with no room for
// its frame is an overflow artifact, and the bytes after it are stale from
// a previous write. The parser's >= guard drops it; parsing the stale
// region as opus otherwise yields OpusException -4 "corrupted stream".
final frames = RingProtocol.parseAudioPayload([2, 0xAA, 0xBB]);
expect(frames, isEmpty);
});
test('drops the boundary-aligned last frame in tightly-packed input (440B exactly)', () {
// 40 frames of [size=10][10B] = 40 * 11 = 440 bytes — the 40th frame would
// end precisely at audio.length. The firmware never emits such a frame, so
// the >= boundary guard drops it: 39 frames survive, the last being #38.
final audio = <int>[];
for (int i = 0; i < 40; i++) {
audio.add(10);
audio.addAll(List.filled(10, i & 0xFF));
}
expect(audio.length, 440);
final frames = RingProtocol.parseAudioPayload(audio);
expect(frames.length, 39);
expect(frames.last, List.filled(10, 38 & 0xFF));
});
});
group('RingRecordReassembler', () {
test('produces no records until 444 bytes accumulate', () {
final r = RingRecordReassembler();
r.append(List.filled(200, 0xAA));
expect(r.drainRecords(), isEmpty);
expect(r.pendingBytes, 200);
r.append(List.filled(243, 0xBB));
expect(r.drainRecords(), isEmpty);
expect(r.pendingBytes, 443);
});
test('emits exactly one record at the 444B boundary', () {
final r = RingRecordReassembler();
r.append(List.filled(444, 0x42));
final records = r.drainRecords();
expect(records.length, 1);
expect(records[0].length, 444);
expect(r.pendingBytes, 0);
});
test('reassembles a record split across two BLE notifications', () {
final r = RingRecordReassembler();
// 300B chunk + 144B chunk = 444B = one record
r.append(List.filled(300, 0x11));
expect(r.drainRecords(), isEmpty);
r.append(List.filled(144, 0x22));
final records = r.drainRecords();
expect(records.length, 1);
expect(records[0].length, 444);
// First 300 bytes are 0x11, next 144 are 0x22
expect(records[0][0], 0x11);
expect(records[0][299], 0x11);
expect(records[0][300], 0x22);
expect(records[0][443], 0x22);
});
test('emits multiple records and buffers leftover bytes', () {
final r = RingRecordReassembler();
// 1000 bytes = 2 records of 444 + 112 leftover
r.append(List.filled(1000, 0x55));
final records = r.drainRecords();
expect(records.length, 2);
expect(records[0].length, 444);
expect(records[1].length, 444);
expect(r.pendingBytes, 112);
});
test('preserves byte order across many small chunks', () {
final r = RingRecordReassembler();
// Build a unique byte pattern across 444 bytes and feed it 1 byte at a time.
final expected = List<int>.generate(444, (i) => i & 0xFF);
for (final b in expected) {
r.append([b]);
}
final records = r.drainRecords();
expect(records.length, 1);
expect(records[0].toList(), expected);
});
test('handles a stream of two records with mid-record split', () {
final r = RingRecordReassembler();
// Build two records: rec1 = 0x01..0x01, rec2 = 0x02..0x02
final rec1 = List.filled(444, 0x01);
final rec2 = List.filled(444, 0x02);
// Chunk 1: all of rec1 + first 100 bytes of rec2
r.append([...rec1, ...rec2.sublist(0, 100)]);
var records = r.drainRecords();
expect(records.length, 1);
expect(records[0][0], 0x01);
expect(r.pendingBytes, 100);
// Chunk 2: remaining 344 bytes of rec2
r.append(rec2.sublist(100));
records = r.drainRecords();
expect(records.length, 1);
expect(records[0].toList(), rec2);
expect(r.pendingBytes, 0);
});
});
group('end-to-end: NOTIFY_DATA reassembly + record decode', () {
test('reconstructs a record split across two NOTIFY_DATA chunks and decodes audio', () {
// Construct one record: ts=0xDEADBEEF, then two 80B opus-like frames.
const ts = 0xDEADBEEF;
final f1 = List<int>.generate(80, (i) => 0xA0 + (i & 0x0F));
final f2 = List<int>.generate(80, (i) => 0x50 + (i & 0x0F));
final audio = <int>[80, ...f1, 80, ...f2];
audio.addAll(List.filled(440 - audio.length, 0));
final record = <int>[(ts >> 24) & 0xFF, (ts >> 16) & 0xFF, (ts >> 8) & 0xFF, ts & 0xFF, ...audio];
expect(record.length, 444);
// Split across two BLE chunks at an arbitrary boundary.
final reassembler = RingRecordReassembler();
reassembler.append(record.sublist(0, 137));
expect(reassembler.drainRecords(), isEmpty);
reassembler.append(record.sublist(137));
final records = reassembler.drainRecords();
expect(records.length, 1);
expect(RingProtocol.readRecordTimestamp(records[0]), ts);
final audioPayload = records[0].sublist(RingProtocol.timestampBytes);
final frames = RingProtocol.parseAudioPayload(audioPayload);
expect(frames.length, 2);
expect(frames[0], f1);
expect(frames[1], f2);
});
});
}