diff --git a/src/kiss.ts b/src/kiss.ts index a6bcada..9d70e1e 100644 --- a/src/kiss.ts +++ b/src/kiss.ts @@ -6,14 +6,14 @@ export const KISS_FESC = 0xdb; export const KISS_TFEND = 0xdc; export const KISS_TFESC = 0xdd; -export type KissFrame = { +export type KISSFrame = { port: number; // 0-15 command: number; // 0 = data, 1 = TX delay, etc. data: Uint8Array; }; // Encode a KISS frame (data only, port 0 by default) -export function encodeKissFrame(data: Uint8Array, port = 0, command = 0): Uint8Array { +export function encodeKISSFrame(data: Uint8Array, port = 0, command = 0): Uint8Array { const out: number[] = [KISS_FEND]; // Command byte: upper 4 bits = port, lower 4 bits = command out.push(((port & 0x0f) << 4) | (command & 0x0f)); @@ -31,7 +31,7 @@ export function encodeKissFrame(data: Uint8Array, port = 0, command = 0): Uint8A } // Decode a KISS frame (returns null if not a valid frame) -export function decodeKissFrame(buf: Uint8Array): KissFrame | null { +export function decodeKISSFrame(buf: Uint8Array): KISSFrame | null { if (buf.length < 3 || buf[0] !== KISS_FEND || buf[buf.length - 1] !== KISS_FEND) return null; let i = 1; const cmd = buf[i++]; @@ -53,14 +53,14 @@ export function decodeKissFrame(buf: Uint8Array): KissFrame | null { } // Async generator to extract KISS frames from a stream of bytes -export async function* kissFrameReader(source: AsyncIterable): AsyncGenerator { +export async function* kissFrameReader(source: AsyncIterable): AsyncGenerator { let buffer: number[] = []; let inFrame = false; for await (const chunk of source) { for (const b of chunk) { if (b === KISS_FEND) { if (inFrame && buffer.length > 0) { - const frame = decodeKissFrame(Uint8Array.from([KISS_FEND, ...buffer, KISS_FEND])); + const frame = decodeKISSFrame(Uint8Array.from([KISS_FEND, ...buffer, KISS_FEND])); if (frame) yield frame; } buffer = []; @@ -73,7 +73,7 @@ export async function* kissFrameReader(source: AsyncIterable): Async } // Write a KISS frame to a sink (e.g., serial port) -export async function kissFrameWriter(sink: (data: Uint8Array) => Promise, frame: KissFrame) { - const buf = encodeKissFrame(frame.data, frame.port, frame.command); +export async function kissFrameWriter(sink: (data: Uint8Array) => Promise, frame: KISSFrame) { + const buf = encodeKISSFrame(frame.data, frame.port, frame.command); await sink(buf); } diff --git a/test/kiss.test.ts b/test/kiss.test.ts new file mode 100644 index 0000000..a6e0bf1 --- /dev/null +++ b/test/kiss.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from "vitest"; +import { + KISS_FEND, + KISS_FESC, + KISS_TFEND, + KISS_TFESC, + encodeKISSFrame, + decodeKISSFrame, + kissFrameReader, + kissFrameWriter, + type KISSFrame, +} from "../src/kiss"; + +const u8 = (arr: number[]) => Uint8Array.from(arr); + +describe("kiss.ts", () => { + it("encodeKISSFrame produces proper FEND wrappers and decodes back", () => { + const data = u8([1, 2, 3]); + const buf = encodeKISSFrame(data); + expect(buf[0]).toBe(KISS_FEND); + expect(buf[buf.length - 1]).toBe(KISS_FEND); + const frame = decodeKISSFrame(buf); + expect(frame).not.toBeNull(); + expect(frame!.port).toBe(0); + expect(frame!.command).toBe(0); + expect(Array.from(frame!.data)).toEqual(Array.from(data)); + }); + + it("encodes and decodes escape sequences correctly", () => { + const data = u8([0x01, KISS_FEND, 0x02, KISS_FESC, 0x03]); + const encoded = encodeKISSFrame(data, 1, 2); + // command byte should encode port=1, command=2 + expect(encoded[1]).toBe(((1 & 0x0f) << 4) | (2 & 0x0f)); + // ensure escaped bytes present + const arr = Array.from(encoded); + // look for FESC TFEND and FESC TFESC sequences + const hasFescTfend = arr.includes(KISS_FESC) && arr.includes(KISS_TFEND); + const hasFescTfesc = arr.includes(KISS_FESC) && arr.includes(KISS_TFESC); + expect(hasFescTfend).toBeTruthy(); + expect(hasFescTfesc).toBeTruthy(); + + const decoded = decodeKISSFrame(encoded)!; + expect(decoded.port).toBe(1); + expect(decoded.command).toBe(2); + expect(Array.from(decoded.data)).toEqual(Array.from(data)); + }); + + it("decodeKISSFrame returns null for invalid frames", () => { + // missing FEND start + expect(decodeKISSFrame(u8([0x00, 0x00, 0x00]))).toBeNull(); + + // invalid escape sequence + const bad = u8([KISS_FEND, 0x00, KISS_FESC, 0x00, KISS_FEND]); + expect(decodeKISSFrame(bad)).toBeNull(); + }); + + it("parses port and command from the command byte", () => { + const port = 3; + const command = 4; + const data = u8([9, 8]); + const cmd = ((port & 0x0f) << 4) | (command & 0x0f); + const buf = u8([KISS_FEND, cmd, ...Array.from(data), KISS_FEND]); + const f = decodeKISSFrame(buf)!; + expect(f.port).toBe(port); + expect(f.command).toBe(command); + expect(Array.from(f.data)).toEqual(Array.from(data)); + }); + + it("kissFrameReader yields frames assembled across chunks and ignores incomplete frames", async () => { + // create two frames and split them across chunks + const a = encodeKISSFrame(u8([1, 2, 3])); + const b = encodeKISSFrame(u8([4, 5])); + + // craft an async iterable that yields multiple chunks, splitting frames + async function* source() { + // first chunk: first half of a + yield u8(Array.from(a.slice(0, 2))); + // second chunk: rest of a plus start of b + yield u8(Array.from(a.slice(2)).concat(Array.from(b.slice(0, 1)))); + // third chunk: rest of b + yield u8(Array.from(b.slice(1))); + } + + const results: KISSFrame[] = []; + for await (const fr of kissFrameReader(source())) { + results.push(fr); + } + expect(results.length).toBe(2); + expect(Array.from(results[0].data)).toEqual([1, 2, 3]); + expect(Array.from(results[1].data)).toEqual([4, 5]); + + // incomplete frame (no trailing FEND) should not yield + async function* sourceIncomplete() { + const partial = Array.from(a.slice(0, a.length - 1)); + yield u8(partial); + } + const got: KISSFrame[] = []; + for await (const fr of kissFrameReader(sourceIncomplete())) got.push(fr); + expect(got.length).toBe(0); + }); + + it("kissFrameWriter calls the sink with the encoded buffer", async () => { + const received: Uint8Array[] = []; + const sink = async (data: Uint8Array) => { + received.push(data); + }; + const frame: KISSFrame = { port: 2, command: 1, data: u8([7, 8, 9]) }; + await kissFrameWriter(sink, frame); + expect(received.length).toBe(1); + const expected = encodeKISSFrame(frame.data, frame.port, frame.command); + expect(Array.from(received[0])).toEqual(Array.from(expected)); + }); +});