Drop FX.25 for now
This commit is contained in:
124
src/frame.ts
124
src/frame.ts
@@ -1,14 +1,20 @@
|
||||
import { Readable } from 'stream';
|
||||
import { IFrame, IAddress } from './frame.types';
|
||||
import { Address } from './address';
|
||||
import { encodeHDLC, crc16Ccitt, verifyAndStripFcs } from './hdlc';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
const trimRight: (str: string) => string = (str) => str.replace(/\s+$/g, '');
|
||||
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;
|
||||
@@ -17,6 +23,8 @@ export class Frame implements IFrame {
|
||||
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);
|
||||
}
|
||||
@@ -36,6 +44,17 @@ export class Frame implements IFrame {
|
||||
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}`;
|
||||
|
||||
@@ -52,7 +71,7 @@ export class Frame implements IFrame {
|
||||
}
|
||||
}
|
||||
|
||||
return `fm ${src} to ${dst} ctl ${ctl}${pidStr}${lenStr}${infoText}`;
|
||||
return `fm ${src} to ${dst}${viaStr} ctl ${ctl}${pidStr}${lenStr}${infoText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,20 +87,27 @@ export class Frame implements IFrame {
|
||||
* @throws {Error} If the payload is malformed, missing required fields, or does not conform to AX.25.
|
||||
*/
|
||||
static fromBytes(payload: Uint8Array): Frame {
|
||||
// Parse AX.25 addresses (each 7 bytes) until last address (bit 0 of SSID byte === 1)
|
||||
// Skip leading null bytes (common in some KISS/TNC output)
|
||||
let offset = 0;
|
||||
const addrs: IAddress[] = [];
|
||||
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 ssidByte = field[6];
|
||||
const isLast = (ssidByte & 0x01) === 1;
|
||||
// construct Address instance from the 7-byte AX.25 address field
|
||||
const addr = Address.fromBytes(field);
|
||||
addrs.push(addr);
|
||||
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
|
||||
@@ -90,8 +116,10 @@ export class Frame implements IFrame {
|
||||
offset += 1;
|
||||
|
||||
// Addresses as Address objects
|
||||
const dst = Address.fromBytes(addrsRaw(payload, 0));
|
||||
const src = Address.fromBytes(addrsRaw(payload, 7));
|
||||
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
|
||||
@@ -101,6 +129,8 @@ export class Frame implements IFrame {
|
||||
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;
|
||||
}
|
||||
@@ -126,6 +156,8 @@ export class Frame implements IFrame {
|
||||
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;
|
||||
}
|
||||
@@ -162,6 +194,8 @@ export class Frame implements IFrame {
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -174,7 +208,7 @@ export class InformationFrame extends Frame {
|
||||
nr: number;
|
||||
pf: number;
|
||||
|
||||
constructor(src: IAddress, dst: IAddress, control: number, info: Uint8Array, 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;
|
||||
@@ -187,7 +221,7 @@ export class SupervisoryFrame extends Frame {
|
||||
nr: number;
|
||||
pf: number;
|
||||
|
||||
constructor(src: IAddress, dst: IAddress, control: number, 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;
|
||||
@@ -199,7 +233,7 @@ export class UnnumberedFrame extends Frame {
|
||||
uType: string;
|
||||
pf: number;
|
||||
|
||||
constructor(src: IAddress, dst: IAddress, control: number, uType: string, pf: number, info?: Uint8Array) {
|
||||
constructor(src: Address, dst: Address, control: number, uType: string, pf: number, info?: Uint8Array) {
|
||||
super(src, dst, control, info);
|
||||
this.uType = uType;
|
||||
this.pf = pf;
|
||||
@@ -306,57 +340,3 @@ export const encodeAX25: (dst: Address | string, src: Address | string, info: Ui
|
||||
parts.push(...info);
|
||||
return Uint8Array.from(parts);
|
||||
}
|
||||
|
||||
export class FX25 {
|
||||
/**
|
||||
* Heuristic detection: if raw AX.25 parsing and simple FCS checks fail,
|
||||
* this may be an FX.25 frame (FEC encoded). This is a lightweight
|
||||
* starter; proper FX.25 requires Reed-Solomon decoding which is
|
||||
* not implemented here.
|
||||
*/
|
||||
static isLikelyFx25(raw: Uint8Array): boolean {
|
||||
try {
|
||||
Frame.fromBytes(raw);
|
||||
return false;
|
||||
} catch (_) {}
|
||||
const f = verifyAndStripFcs(raw);
|
||||
if (f.ok) {
|
||||
try {
|
||||
Frame.fromBytes(f.payload!);
|
||||
return false;
|
||||
} catch (_) {}
|
||||
}
|
||||
return raw.length > 0; // conservative: could be FX.25
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decode FX.25 to AX.25 frames. Currently tries plain AX.25
|
||||
* and FCS-stripped AX.25. A full FX.25 (FEC) implementation would go
|
||||
* where the TODO is.
|
||||
*/
|
||||
static decode(raw: Uint8Array): Frame[] {
|
||||
// try raw
|
||||
try {
|
||||
return [Frame.fromBytes(raw)];
|
||||
} catch (_) {
|
||||
// continue
|
||||
}
|
||||
|
||||
// try strip FCS
|
||||
const res = verifyAndStripFcs(raw);
|
||||
if (res.ok && res.payload) {
|
||||
try {
|
||||
return [Frame.fromBytes(res.payload)];
|
||||
} catch (_) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement FX.25 FEC decode (Reed-Solomon) here
|
||||
throw new Error('FX25.decode: FEC decoding not implemented');
|
||||
}
|
||||
|
||||
static computeCheck(raw: Uint8Array): number {
|
||||
return crc16Ccitt(raw);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user