2026-04-08 18:37:08 +02:00

125 lines
4.6 KiB
TypeScript

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);
});
});
});