import { Readable } from 'stream'; import { IFrame, IAddress } from './frame.types'; import { Address } from './address'; import { encodeHDLC } from './hdlc'; /** * Represents an AX.25 frame with source/destination addresses, control field, and information field. * * This class serves as a base for specific frame types (InformationFrame, SupervisoryFrame, UnnumberedFrame) * and provides common functionality such as parsing from raw bytes and encoding to AX.25 format. */ export class Frame implements IFrame { destination: IAddress; source: IAddress; repeaters: IAddress[]; repeatedIndex: number | null; control: number; pid?: number; info: Uint8Array; raw?: Uint8Array; constructor(src: Address | string = '', dst: Address | string = '', control = 0, info?: Uint8Array) { this.source = typeof src === 'string' ? Address.fromString(src) : src; this.destination = typeof dst === 'string' ? Address.fromString(dst) : dst; this.repeaters = []; this.repeatedIndex = null; this.control = control; this.info = info ?? new Uint8Array(0); } toString() { // Format similar to common packet-radio programs const src = this.source.toString(); const dst = this.destination.toString(); let ctl = ''; if (this instanceof UnnumberedFrame) { ctl = `${this.uType}${this.pf ? '^' : '-'}`; } else if (this instanceof SupervisoryFrame) { ctl = `${this.sType}${this.nr}${this.pf ? 'v' : '-'}`; } else if (this instanceof InformationFrame) { ctl = `${this.ns}${this.nr}${this.pf ? '^' : '-'}`; } else { ctl = `CTL${this.control.toString(16)}`; } // Repeater path formatting let viaStr = ''; if (this.repeaters && this.repeaters.length > 0) { const path = this.repeaters.map((repeater, i) => { let s = repeater.toString(); if (this.repeatedIndex === i) s += '*'; return s; }).join(','); viaStr = ` via ${path}`; } const pidStr = typeof this.pid === 'number' ? ` pid ${this.pid.toString(16).toUpperCase().padStart(2, '0')}` : ''; const lenStr = ` len=${this.info?.length ?? 0}`; // If info looks like printable ASCII, append it after a colon let infoText = ''; if (this.info && this.info.length > 0) { const printable = Array.from(this.info).every(b => (b >= 0x20 && b <= 0x7e) || b === 0x09 || b === 0x0a || b === 0x0d); if (printable) { try { infoText = `: ${new TextDecoder().decode(this.info)}`; } catch { infoText = ''; } } } return `fm ${src} to ${dst}${viaStr} ctl ${ctl}${pidStr}${lenStr}${infoText}`; } /** * Parses a given AX.25 frame payload and returns the corresponding Frame instance. * * This function processes the AX.25 protocol frame structure, extracting address fields, * control field, and information field as appropriate. It supports parsing of I-frames, * S-frames, and U-frames, returning the correct subclass of `Frame` for each. * * @param payload - The raw AX.25 frame as a Uint8Array. * @returns {Frame} An instance of `InformationFrame`, `SupervisoryFrame`, or `UnnumberedFrame` * depending on the type of AX.25 frame parsed. * @throws {Error} If the payload is malformed, missing required fields, or does not conform to AX.25. */ static fromBytes(payload: Uint8Array): Frame { // Skip leading null bytes (common in some KISS/TNC output) let offset = 0; while (offset < payload.length && payload[offset] === 0x00) { offset++; } // Parse AX.25 addresses (each 7 bytes) until last address (bit 0 of SSID byte === 1) const addrs: Address[] = []; const repeaterFlags: boolean[] = []; while (offset + 7 <= payload.length) { const field = payload.slice(offset, offset + 7); const addr = Address.fromBytes(field); const isLast = addr.getExtension(); const hasRepeated = addr.getCH(); addrs.push(addr); repeaterFlags.push(hasRepeated); offset += 7; if (isLast) break; } // Need at least destination and source addresses if (addrs.length < 2) throw new Error('AX.25: not enough address fields'); // next byte: control if (offset >= payload.length) throw new Error('AX.25: missing control byte'); const control = payload[offset]; offset += 1; // Addresses as Address objects const dst: Address = addrs[0]; const src: Address = addrs[1]; const repeaters: Address[] = addrs.length > 2 ? addrs.slice(2) : []; const repeatedIndex = repeaterFlags.length > 2 ? repeaterFlags.slice(2).lastIndexOf(true) : -1; // Decode control field (only 1-byte control supported here) // I-frame: bit0 == 0 if ((control & 0x01) === 0) { const ns = (control >> 1) & 0x07; const pf = (control >> 4) & 0x01; const nr = (control >> 5) & 0x07; const info = payload.slice(offset); const f = new InformationFrame(src, dst, control, info, ns, nr, pf); f.repeaters = repeaters; f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null; f.raw = payload; return f; } // S-frame: bits 0-1 == 01 if ((control & 0x03) === 0x01) { const sCode = (control >> 2) & 0x03; const pf = (control >> 4) & 0x01; const nr = (control >> 5) & 0x07; let sType: 'RR' | 'RNR' | 'REJ' | 'SREJ' = 'RR'; switch (sCode) { case 0: sType = 'RR'; break; case 1: sType = 'RNR'; break; case 2: sType = 'REJ'; break; case 3: sType = 'SREJ'; break; } const f = new SupervisoryFrame(src, dst, control, sType, nr, pf); f.repeaters = repeaters; f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null; f.raw = payload; return f; } // U-frame: bits 0-1 == 11 { const pf = (control >> 4) & 0x01; // common U-frame types (not exhaustive) let uType = `U(${control.toString(16)})`; switch (control) { case 0x03: uType = 'UI'; break; case 0x2f: uType = 'SABM'; break; case 0x63: uType = 'UA'; break; case 0x43: uType = 'DISC'; break; case 0x0f: uType = 'SNRM'; break; } let uPid: number | undefined; let info: Uint8Array = new Uint8Array(0); if (uType === 'UI') { if (offset < payload.length) { uPid = payload[offset]; offset += 1; info = payload.slice(offset); } } const f = new UnnumberedFrame(src, dst, control, uType, pf, info); f.repeaters = repeaters; f.repeatedIndex = repeatedIndex >= 0 ? repeatedIndex : null; f.pid = uPid; f.raw = payload; return f; } } } export class InformationFrame extends Frame { ns: number; nr: number; pf: number; constructor(src: Address, dst: Address, control: number, info: Uint8Array, ns: number, nr: number, pf: number) { super(src, dst, control, info); this.ns = ns; this.nr = nr; this.pf = pf; } } export class SupervisoryFrame extends Frame { sType: 'RR' | 'RNR' | 'REJ' | 'SREJ'; nr: number; pf: number; constructor(src: Address, dst: Address, control: number, sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf: number) { super(src, dst, control, new Uint8Array(0)); this.sType = sType; this.nr = nr; this.pf = pf; } } export class UnnumberedFrame extends Frame { uType: string; pf: number; constructor(src: Address, dst: Address, control: number, uType: string, pf: number, info?: Uint8Array) { super(src, dst, control, info); this.uType = uType; this.pf = pf; } } export const buildIControl: (ns: number, nr: number, pf?: number) => number = (ns, nr, pf = 0) => { // bit0 = 0 for I-frames; ns in bits 1-3, pf in bit4, nr in bits 5-7 return ((ns & 0x07) << 1) | ((pf & 0x01) << 4) | ((nr & 0x07) << 5); }; export const buildSControl: (sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf?: number) => number = (sType, nr, pf = 0) => { const sCode = sType === 'RR' ? 0 : sType === 'RNR' ? 1 : sType === 'REJ' ? 2 : 3; // bits 0-1 = 01 return 0x01 | (sCode << 2) | ((pf & 0x01) << 4) | ((nr & 0x07) << 5); }; export const buildUControl: (uType: string, pf?: number) => number = (uType, pf = 0) => { // common mappings switch (uType) { case 'UI': return 0x03 | ((pf & 0x01) << 4); case 'SABM': return 0x2f | ((pf & 0x01) << 4); case 'UA': return 0x63 | ((pf & 0x01) << 4); case 'DISC': return 0x43 | ((pf & 0x01) << 4); default: return 0x03 | ((pf & 0x01) << 4); } }; // Extended control builders (2-byte) - minimal support for encoding larger NS/NR export const buildIControlExtended: (ns: number, nr: number, pf?: number) => Uint8Array = (ns, nr, pf = 0) => { const b1 = ((ns & 0x7f) << 1) | ((pf & 0x01) << 4); const b2 = ((nr & 0x7f) << 1); return Uint8Array.from([b1 & 0xff, b2 & 0xff]); }; export const buildSControlExtended: (sType: 'RR' | 'RNR' | 'REJ' | 'SREJ', nr: number, pf?: number) => Uint8Array = (sType, nr, pf = 0) => { const sCode = sType === 'RR' ? 0 : sType === 'RNR' ? 1 : sType === 'REJ' ? 2 : 3; const b1 = 0x01 | (sCode << 2) | ((pf & 0x01) << 4); const b2 = ((nr & 0x7f) << 1); return Uint8Array.from([b1 & 0xff, b2 & 0xff]); }; export const encodeFrame: (frame: Frame) => Uint8Array = (frame) => { const parts: number[] = []; // addresses: destination then source; mark source as last parts.push(...(frame.destination as IAddress).toBytes(false)); parts.push(...(frame.source as IAddress).toBytes(true)); // control parts.push(frame.control & 0xff); if (frame instanceof UnnumberedFrame) { if (frame.uType === 'UI') { parts.push(frame.pid ?? 0xf0); parts.push(...(frame.info ?? new Uint8Array(0))); } // other U-frames have no pid/info in AX.25 common usage } else if (frame instanceof InformationFrame) { parts.push(...(frame.info ?? new Uint8Array(0))); } else if (frame instanceof SupervisoryFrame) { // S-frames typically carry no PID/info } return Uint8Array.from(parts); }; export const toHDLC: (frame: Frame) => Uint8Array = (frame) => { const ax25 = encodeFrame(frame); return encodeHDLC(ax25, { includeFcs: true }); }; export const toWire: (frame: Frame) => Buffer = (frame) => { const h = toHDLC(frame); return Buffer.from(h.buffer, h.byteOffset, h.byteLength); }; export const toWireStream: (frame: Frame) => Readable = (frame) => { const buf = toWire(frame); const r = new Readable(); r.push(buf); r.push(null); return r; }; const addrsRaw: (payload: Uint8Array, offset: number) => Uint8Array = (payload, offset) => payload.slice(offset, offset + 7); export const encodeAX25: (dst: Address | string, src: Address | string, info: Uint8Array, options?: { ui?: boolean }) => Uint8Array = (dst, src, info, options) => { // dst and src should be like 'CALL-SSID' or 'CALL' const dstAddr = typeof dst === 'string' ? Address.fromString(dst) : dst; const srcAddr = typeof src === 'string' ? Address.fromString(src) : src; const parts: number[] = []; const dfield = dstAddr.toBytes(false); const sfield = srcAddr.toBytes(true); // mark source as last address parts.push(...dfield, ...sfield); // control parts.push(options?.ui ? 0x03 : 0x03); // PID - use 0xf0 (no layer 3) as default for UI-frames parts.push(0xf0); parts.push(...info); return Uint8Array.from(parts); }