diff --git a/src/kiss.ts b/src/kiss.ts new file mode 100644 index 0000000..a6bcada --- /dev/null +++ b/src/kiss.ts @@ -0,0 +1,79 @@ +// KISS (Keep It Simple Stupid) frame reader/writer for AX.25 +// See: http://www.ax25.net/kiss.aspx + +export const KISS_FEND = 0xc0; +export const KISS_FESC = 0xdb; +export const KISS_TFEND = 0xdc; +export const KISS_TFESC = 0xdd; + +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 { + const out: number[] = [KISS_FEND]; + // Command byte: upper 4 bits = port, lower 4 bits = command + out.push(((port & 0x0f) << 4) | (command & 0x0f)); + for (const b of data) { + if (b === KISS_FEND) { + out.push(KISS_FESC, KISS_TFEND); + } else if (b === KISS_FESC) { + out.push(KISS_FESC, KISS_TFESC); + } else { + out.push(b); + } + } + out.push(KISS_FEND); + return Uint8Array.from(out); +} + +// Decode a KISS frame (returns null if not a valid frame) +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++]; + const port = (cmd >> 4) & 0x0f; + const command = cmd & 0x0f; + const data: number[] = []; + while (i < buf.length - 1) { + const b = buf[i++]; + if (b === KISS_FESC) { + const next = buf[i++]; + if (next === KISS_TFEND) data.push(KISS_FEND); + else if (next === KISS_TFESC) data.push(KISS_FESC); + else return null; // Invalid escape + } else { + data.push(b); + } + } + return { port, command, data: Uint8Array.from(data) }; +} + +// Async generator to extract KISS frames from a stream of bytes +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])); + if (frame) yield frame; + } + buffer = []; + inFrame = true; + } else if (inFrame) { + buffer.push(b); + } + } + } +} + +// 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); + await sink(buf); +}