Consistent KISS naming and tests
This commit is contained in:
14
src/kiss.ts
14
src/kiss.ts
@@ -6,14 +6,14 @@ export const KISS_FESC = 0xdb;
|
|||||||
export const KISS_TFEND = 0xdc;
|
export const KISS_TFEND = 0xdc;
|
||||||
export const KISS_TFESC = 0xdd;
|
export const KISS_TFESC = 0xdd;
|
||||||
|
|
||||||
export type KissFrame = {
|
export type KISSFrame = {
|
||||||
port: number; // 0-15
|
port: number; // 0-15
|
||||||
command: number; // 0 = data, 1 = TX delay, etc.
|
command: number; // 0 = data, 1 = TX delay, etc.
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encode a KISS frame (data only, port 0 by default)
|
// 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];
|
const out: number[] = [KISS_FEND];
|
||||||
// Command byte: upper 4 bits = port, lower 4 bits = command
|
// Command byte: upper 4 bits = port, lower 4 bits = command
|
||||||
out.push(((port & 0x0f) << 4) | (command & 0x0f));
|
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)
|
// 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;
|
if (buf.length < 3 || buf[0] !== KISS_FEND || buf[buf.length - 1] !== KISS_FEND) return null;
|
||||||
let i = 1;
|
let i = 1;
|
||||||
const cmd = buf[i++];
|
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
|
// Async generator to extract KISS frames from a stream of bytes
|
||||||
export async function* kissFrameReader(source: AsyncIterable<Uint8Array>): AsyncGenerator<KissFrame> {
|
export async function* kissFrameReader(source: AsyncIterable<Uint8Array>): AsyncGenerator<KISSFrame> {
|
||||||
let buffer: number[] = [];
|
let buffer: number[] = [];
|
||||||
let inFrame = false;
|
let inFrame = false;
|
||||||
for await (const chunk of source) {
|
for await (const chunk of source) {
|
||||||
for (const b of chunk) {
|
for (const b of chunk) {
|
||||||
if (b === KISS_FEND) {
|
if (b === KISS_FEND) {
|
||||||
if (inFrame && buffer.length > 0) {
|
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;
|
if (frame) yield frame;
|
||||||
}
|
}
|
||||||
buffer = [];
|
buffer = [];
|
||||||
@@ -73,7 +73,7 @@ export async function* kissFrameReader(source: AsyncIterable<Uint8Array>): Async
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write a KISS frame to a sink (e.g., serial port)
|
// Write a KISS frame to a sink (e.g., serial port)
|
||||||
export async function kissFrameWriter(sink: (data: Uint8Array) => Promise<void>, frame: KissFrame) {
|
export async function kissFrameWriter(sink: (data: Uint8Array) => Promise<void>, frame: KISSFrame) {
|
||||||
const buf = encodeKissFrame(frame.data, frame.port, frame.command);
|
const buf = encodeKISSFrame(frame.data, frame.port, frame.command);
|
||||||
await sink(buf);
|
await sink(buf);
|
||||||
}
|
}
|
||||||
|
|||||||
113
test/kiss.test.ts
Normal file
113
test/kiss.test.ts
Normal file
@@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user