Preserve C/H bits and extension bit

This commit is contained in:
2026-03-12 09:25:53 +01:00
parent 5bf12c326a
commit 4dc3118806
3 changed files with 121 additions and 8 deletions

View File

@@ -1,8 +1,11 @@
export class Address { export class Address {
callsign: string; public readonly callsign: string;
ssid: number; 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'); if (typeof callsign !== 'string') throw new TypeError('callsign must be a string');
const norm = callsign.toUpperCase().trim(); const norm = callsign.toUpperCase().trim();
if (norm.length === 0) throw new Error('callsign must not be empty'); 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'); if (ssid < 0 || ssid > 15) throw new RangeError('ssid must be between 0 and 15');
this.ssid = ssid & 0x0f; this.ssid = ssid & 0x0f;
this.ch = !!ch;
this.reserved = reserved & 0b11;
this.extension = !!extension;
} }
toString(opts?: { sep?: string; showSsid?: boolean }) { toString(opts?: { sep?: string; showSsid?: boolean }) {
@@ -36,13 +42,18 @@ export class Address {
return new Address(call, ssid); return new Address(call, ssid);
} }
toBytes(last = false): Uint8Array { toBytes(last = this.extension): Uint8Array {
const buf = new Uint8Array(7); const buf = new Uint8Array(7);
const cs = this.callsign.padEnd(6, ' ').slice(0, 6); const cs = this.callsign.padEnd(6, ' ').slice(0, 6);
for (let i = 0; i < 6; i++) buf[i] = cs.charCodeAt(i) << 1; 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 // Compose SSID byte: bits 7 (C/H), 6-5 (reserved), 4-1 (SSID), 0 (last)
buf[6] = 0x60 | ((this.ssid & 0x0f) << 1); buf[6] =
if (last) buf[6] |= 0x01; // mark as last address (HDLC extension bit) (this.ch ? 0x80 : 0x00) |
((this.reserved & 0b11) << 5) |
((this.ssid & 0x0f) << 1);
if (last) {
buf[6] |= 0x01;
}
return buf; return buf;
} }
@@ -52,6 +63,58 @@ export class Address {
for (let i = 0; i < 6; i++) chars.push(buf[i] >> 1); for (let i = 0; i < 6; i++) chars.push(buf[i] >> 1);
const callsign = String.fromCharCode(...chars).trim(); const callsign = String.fromCharCode(...chars).trim();
const ssid = (buf[6] >> 1) & 0x0f; 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;
} }
} }

View File

@@ -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);
});
});

View File

@@ -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);
});
});