diff --git a/src/address.ts b/src/address.ts index 5a8c1c5..47cd028 100644 --- a/src/address.ts +++ b/src/address.ts @@ -1,8 +1,11 @@ export class Address { - callsign: string; - ssid: number; + public readonly callsign: string; + public readonly ssid: number; + private ch: boolean; // C (Command/Response) or H (Has-Been-Repeated) + private reserved: number; // Bits 5 and 6 (should be 0b11 normally) + private extension: boolean; // LSB: true if last address field - constructor(callsign: string, ssid = 0) { + constructor(callsign: string, ssid = 0, ch = false, reserved = 0b11, extension = false) { if (typeof callsign !== 'string') throw new TypeError('callsign must be a string'); const norm = callsign.toUpperCase().trim(); if (norm.length === 0) throw new Error('callsign must not be empty'); @@ -14,6 +17,9 @@ export class Address { } if (ssid < 0 || ssid > 15) throw new RangeError('ssid must be between 0 and 15'); this.ssid = ssid & 0x0f; + this.ch = !!ch; + this.reserved = reserved & 0b11; + this.extension = !!extension; } toString(opts?: { sep?: string; showSsid?: boolean }) { @@ -36,13 +42,18 @@ export class Address { return new Address(call, ssid); } - toBytes(last = false): Uint8Array { + toBytes(last = this.extension): Uint8Array { const buf = new Uint8Array(7); const cs = this.callsign.padEnd(6, ' ').slice(0, 6); for (let i = 0; i < 6; i++) buf[i] = cs.charCodeAt(i) << 1; - // Per AX.25 spec the SSID byte contains reserved bits; commonly 0x60 - buf[6] = 0x60 | ((this.ssid & 0x0f) << 1); - if (last) buf[6] |= 0x01; // mark as last address (HDLC extension bit) + // Compose SSID byte: bits 7 (C/H), 6-5 (reserved), 4-1 (SSID), 0 (last) + buf[6] = + (this.ch ? 0x80 : 0x00) | + ((this.reserved & 0b11) << 5) | + ((this.ssid & 0x0f) << 1); + if (last) { + buf[6] |= 0x01; + } return buf; } @@ -52,6 +63,58 @@ export class Address { for (let i = 0; i < 6; i++) chars.push(buf[i] >> 1); const callsign = String.fromCharCode(...chars).trim(); const ssid = (buf[6] >> 1) & 0x0f; - return new Address(callsign, ssid); + const ch = (buf[6] & 0x80) !== 0; + const reserved = (buf[6] >> 5) & 0b11; + const extension = (buf[6] & 0x01) === 1; + return new Address(callsign, ssid, ch, reserved, extension); + } + /** + * Returns true if the extension bit (LSB) is set (this is the last address field) + */ + getExtensionBit(): boolean { + return this.extension; + } + + /** + * Set the extension bit (LSB) for this address + */ + setExtensionBit(val: boolean): void { + this.extension = !!val; + } + + /** + * Returns true if the C/H bit is set (Command/Response for src/dst, Has-Been-Repeated for repeaters) + */ + getCH(): boolean { + return this.ch; + } + + /** + * Returns the reserved bits (should be 0b11 for normal AX.25) + */ + getReserved(): number { + return this.reserved; + } + + /** + * Returns true if this is the last address field (extension bit set) + */ + getExtension(): boolean { + return this.extension; + } + + /** + * Set the extension (LSB) for this address + */ + setExtension(val: boolean): void { + this.extension = !!val; + } + + /** + * Returns true if the extension (LSB) is set in the given buffer (static helper) + */ + static isLastAddress(buf: Uint8Array): boolean { + if (buf.length < 7) throw new Error('Address.isLastAddress: buffer too short'); + return (buf[6] & 0x01) === 1; } } diff --git a/test/address.chbit.test.ts b/test/address.chbit.test.ts new file mode 100644 index 0000000..b51b4b1 --- /dev/null +++ b/test/address.chbit.test.ts @@ -0,0 +1,26 @@ +import { Address } from "../src/address"; +import { describe, it, expect } from "vitest"; + +describe("Address C/H and reserved bits", () => { + it("encodes and decodes C/H and reserved", () => { + const addr = new Address("NOCALL", 7, true, 0b10); + const bytes = addr.toBytes(); + expect((bytes[6] & 0x80) !== 0).toBe(true); // C/H + expect(((bytes[6] >> 5) & 0b11)).toBe(0b10); // reserved + expect(((bytes[6] >> 1) & 0x0f)).toBe(7); // SSID + const decoded = Address.fromBytes(bytes); + expect(decoded.callsign).toBe("NOCALL"); + expect(decoded.ssid).toBe(7); + expect(decoded.getCH()).toBe(true); + expect(decoded.getReserved()).toBe(0b10); + }); + + it("sets and detects last address extension", () => { + const addr = new Address("TEST", 1, false, 0b11); + const bytes = addr.toBytes(true); + expect(Address.isLastAddress(bytes)).toBe(true); + expect((bytes[6] & 0x01)).toBe(1); + const notLast = addr.toBytes(false); + expect(Address.isLastAddress(notLast)).toBe(false); + }); +}); diff --git a/test/address.extensionbit.test.ts b/test/address.extensionbit.test.ts new file mode 100644 index 0000000..05d8e24 --- /dev/null +++ b/test/address.extensionbit.test.ts @@ -0,0 +1,24 @@ +import { Address } from "../src/address"; +import { describe, it, expect } from "vitest"; + +describe("Address extension handling", () => { + it("encodes and decodes extension", () => { + const addr = new Address("NOCALL", 7, false, 0b11, true); + const bytes = addr.toBytes(); + expect((bytes[6] & 0x01)).toBe(1); // extension set + const decoded = Address.fromBytes(bytes); + expect(decoded.getExtension()).toBe(true); + expect(decoded.callsign).toBe("NOCALL"); + expect(decoded.ssid).toBe(7); + }); + + it("can set and clear extension", () => { + const addr = new Address("TEST", 1); + addr.setExtension(true); + let bytes = addr.toBytes(); + expect((bytes[6] & 0x01)).toBe(1); + addr.setExtension(false); + bytes = addr.toBytes(); + expect((bytes[6] & 0x01)).toBe(0); + }); +});