import { fromBinary, toBinary } from '@bufbuild/protobuf'; import * as P from '@meshtastic/protobufs'; import { describe, expect, it } from 'vitest'; import { buildTextToRadio, extractFrames, frame } from './codec.js'; const F = (payload: number[]) => [ 0x94, 0xc3, (payload.length >> 8) & 0xff, payload.length & 0xff, ...payload, ]; describe('extractFrames', () => { it('emits one complete frame and empties rest', () => { const { packets, rest } = extractFrames(Uint8Array.from(F([1, 2, 3]))); expect(packets).toHaveLength(1); expect(Array.from(packets[0])).toEqual([1, 2, 3]); expect(rest.length).toBe(0); }); it('emits two frames from one chunk', () => { const buf = Uint8Array.from([...F([1]), ...F([2, 2])]); const { packets, rest } = extractFrames(buf); expect(packets.map((p) => Array.from(p))).toEqual([[1], [2, 2]]); expect(rest.length).toBe(0); }); it('reassembles a frame split across two feedings', () => { const full = Uint8Array.from(F([9, 8, 7])); const first = extractFrames(full.subarray(0, 5)); // header + 1 payload byte expect(first.packets).toHaveLength(0); expect(Array.from(first.rest)).toEqual(Array.from(full.subarray(0, 5))); const second = extractFrames( Uint8Array.from([...first.rest, ...full.subarray(5)]), ); expect(second.packets.map((p) => Array.from(p))).toEqual([[9, 8, 7]]); expect(second.rest.length).toBe(0); }); it('discards garbage bytes before the header', () => { const buf = Uint8Array.from([0xaa, 0xbb, ...F([5])]); const { packets, rest } = extractFrames(buf); expect(packets.map((p) => Array.from(p))).toEqual([[5]]); expect(rest.length).toBe(0); }); it('resyncs past a bogus len > 1024 and finds the real frame', () => { const buf = Uint8Array.from([0x94, 0xc3, 0xff, 0xfe, ...F([7, 6])]); const { packets, rest } = extractFrames(buf); expect(packets.map((p) => Array.from(p))).toEqual([[7, 6]]); expect(rest.length).toBe(0); }); it('keeps a trailing lone 0x94 in rest', () => { const { packets, rest } = extractFrames(Uint8Array.from([0xaa, 0x94])); expect(packets).toHaveLength(0); expect(Array.from(rest)).toEqual([0x94]); }); }); describe('frame', () => { it('prepends the 0x94 0xC3 big-endian length header', () => { const out = frame(Uint8Array.from([1, 2, 3])); expect(Array.from(out)).toEqual([0x94, 0xc3, 0x00, 0x03, 1, 2, 3]); }); it('encodes lengths above 255 in two bytes', () => { const payload = new Uint8Array(300).fill(7); const out = frame(payload); expect(Array.from(out.subarray(0, 4))).toEqual([0x94, 0xc3, 0x01, 0x2c]); expect(out.length).toBe(304); }); it('round-trips through extractFrames', () => { const framed = frame(Uint8Array.from([4, 5])); const { packets, rest } = extractFrames(framed); expect(packets.map((p) => Array.from(p))).toEqual([[4, 5]]); expect(rest.length).toBe(0); }); }); // re-serialize just the nested Data message so its wire bytes are inspectable without the random packet id interfering function dataBytes(framed: Uint8Array): Uint8Array { const { packets } = extractFrames(framed); // buildTextToRadio returns framed bytes expect(packets).toHaveLength(1); const tr = fromBinary(P.Mesh.ToRadioSchema, packets[0]); if (tr.payloadVariant?.case !== 'packet') throw new Error('not a packet'); const pv = tr.payloadVariant.value.payloadVariant; if (pv?.case !== 'decoded') throw new Error('not decoded'); return toBinary(P.Mesh.DataSchema, pv.value); } describe('buildTextToRadio', () => { it('writes wire tag 0x3d (Data.reply_id, field 7) iff replyId is given', async () => { const withReply = await buildTextToRadio(42, 'hi', 3, 42); expect(Array.from(dataBytes(withReply.bytes))).toContain(0x3d); // omitting it must leave the field off the wire (proto3 default 0) — snake_case reply_id would be silently dropped here const plain = await buildTextToRadio(42, 'hi', 3); expect(Array.from(dataBytes(plain.bytes))).not.toContain(0x3d); }); });