import { describe, it, expect } from 'vitest'; import { crc8, encodeFrame, FrameDecoder, FRAME_START, MsgType, Emotion, type DecodedFrame, } from '../../src/hardware/protocol.js'; describe('hardware/protocol', () => { describe('crc8', () => { it('returns 0 for an empty buffer', () => { expect(crc8(Buffer.alloc(0))).toBe(0); }); it('matches the known-good value for a single byte', () => { // CRC-8/SMBus (poly 0x07, init 0x00) of [0x00] is 0x00. expect(crc8(Buffer.from([0x00]))).toBe(0x00); // CRC-8 of [0x01] = 0x07. expect(crc8(Buffer.from([0x01]))).toBe(0x07); // CRC-8 of [0xAA] = 0x5F. expect(crc8(Buffer.from([0xaa]))).toBe(0x5f); }); it('is stable across reference inputs', () => { // The string "123456789" is the canonical CRC test vector — // CRC-8 (poly 0x07, init 0x00) = 0xF4. expect(crc8(Buffer.from('123456789', 'ascii'))).toBe(0xf4); }); }); describe('encodeFrame', () => { it('builds a well-formed DISPLAY_EMOTION frame', () => { const frame = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])); expect(frame[0]).toBe(FRAME_START); expect(frame[1]).toBe(MsgType.DISPLAY_EMOTION); expect(frame[2]).toBe(0x00); // length high byte expect(frame[3]).toBe(0x01); // length low byte expect(frame[4]).toBe(Emotion.HAPPY); // The last byte is CRC8 over bytes [1..5). expect(frame[5]).toBe(crc8(frame, 1, 5)); expect(frame.length).toBe(6); }); it('handles zero-length payloads (DISPLAY_CLEAR, STATUS)', () => { const frame = encodeFrame(MsgType.DISPLAY_CLEAR); expect(frame.length).toBe(5); expect(frame[0]).toBe(FRAME_START); expect(frame[1]).toBe(MsgType.DISPLAY_CLEAR); expect(frame[2]).toBe(0x00); expect(frame[3]).toBe(0x00); // CRC over just [type, 0, 0]. expect(frame[4]).toBe(crc8(Buffer.from([MsgType.DISPLAY_CLEAR, 0, 0]))); }); it('rejects oversized payloads', () => { expect(() => encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.alloc(2048))).toThrow(); }); }); describe('FrameDecoder', () => { it('decodes a frame fed in one chunk', () => { const received: DecodedFrame[] = []; const decoder = new FrameDecoder((f) => received.push(f)); decoder.feed(encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.LOVE]))); expect(received).toHaveLength(1); expect(received[0]!.type).toBe(MsgType.DISPLAY_EMOTION); expect(received[0]!.payload[0]).toBe(Emotion.LOVE); expect(decoder.framesOk).toBe(1); expect(decoder.framesDropped).toBe(0); }); it('decodes a frame fed byte-by-byte', () => { const received: DecodedFrame[] = []; const decoder = new FrameDecoder((f) => received.push(f)); const frame = encodeFrame(MsgType.PONG, Buffer.from('hello')); for (const b of frame) decoder.feed(Buffer.from([b])); expect(received).toHaveLength(1); expect(received[0]!.type).toBe(MsgType.PONG); expect(received[0]!.payload.toString('utf8')).toBe('hello'); }); it('resyncs after a corrupted CRC', () => { const received: DecodedFrame[] = []; const decoder = new FrameDecoder((f) => received.push(f)); const bad = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.SAD])); bad[bad.length - 1] = (bad[bad.length - 1]! ^ 0xff) & 0xff; // flip CRC decoder.feed(bad); const good = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])); decoder.feed(good); expect(received).toHaveLength(1); expect(received[0]!.payload[0]).toBe(Emotion.HAPPY); expect(decoder.framesDropped).toBe(1); expect(decoder.framesOk).toBe(1); }); it('ignores noise before the start byte', () => { const received: DecodedFrame[] = []; const decoder = new FrameDecoder((f) => received.push(f)); decoder.feed(Buffer.from([0x00, 0x12, 0x34, 0xff])); // garbage decoder.feed(encodeFrame(MsgType.PING)); expect(received).toHaveLength(1); expect(received[0]!.type).toBe(MsgType.PING); }); it('handles two back-to-back frames in one chunk', () => { const received: DecodedFrame[] = []; const decoder = new FrameDecoder((f) => received.push(f)); const buf = Buffer.concat([ encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])), encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.WINK])), ]); decoder.feed(buf); expect(received).toHaveLength(2); expect(received[0]!.payload[0]).toBe(Emotion.HAPPY); expect(received[1]!.payload[0]).toBe(Emotion.WINK); }); }); });