Preserve C/H bits and extension bit
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
test/address.chbit.test.ts
Normal file
26
test/address.chbit.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
24
test/address.extensionbit.test.ts
Normal file
24
test/address.extensionbit.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user