Major change: switched to DataType enum

This commit is contained in:
2026-03-15 22:57:19 +01:00
parent 074806528f
commit c300aefc0b
7 changed files with 260 additions and 216 deletions

View File

@@ -1,12 +1,14 @@
import { describe, expect, it } from "vitest";
import { Address, Frame, Timestamp } from "../src/frame";
import type {
Payload,
PositionPayload,
ObjectPayload,
StatusPayload,
ITimestamp,
MessagePayload,
import {
type Payload,
type PositionPayload,
type ObjectPayload,
type StatusPayload,
type ITimestamp,
type MessagePayload,
DataType,
MicEPayload,
} from "../src/frame.types";
import { Dissected, FieldType } from "@hamradio/packet";
@@ -79,7 +81,7 @@ describe("Frame.decode (basic)", () => {
const frame = Frame.fromString(data);
const decoded = frame.decode() as PositionPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
});
it("should handle various data type identifiers without throwing", () => {
@@ -217,17 +219,17 @@ describe("Frame.decodeMicE", () => {
it("decodes a basic Mic-E packet (current format)", () => {
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.MicECurrent);
});
it("decodes a Mic-E packet with old format (single quote)", () => {
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.MicEOld);
});
});
@@ -237,7 +239,7 @@ describe("Frame.decodeMessage", () => {
const frame = Frame.fromString(raw);
const decoded = frame.decode() as MessagePayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("message");
expect(decoded?.type).toBe(DataType.Message);
expect(decoded?.addressee).toBe("KB1ABC-5");
expect(decoded?.text).toBe("Hello World");
});
@@ -250,7 +252,7 @@ describe("Frame.decodeMessage", () => {
structure: Dissected;
};
expect(res.payload).not.toBeNull();
expect(res.payload?.type).toBe("message");
expect(res.payload?.type).toBe(DataType.Message);
const recipientSection = res.structure.find((s) => s.name === "recipient");
const textSection = res.structure.find((s) => s.name === "text");
expect(recipientSection).toBeDefined();
@@ -266,10 +268,10 @@ describe("Frame.decodeObject", () => {
it("decodes object payload with uncompressed position", () => {
const data = "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as ObjectPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("object");
if (decoded && decoded.type === "object") {
expect(decoded?.type).toBe(DataType.Object);
if (decoded && decoded.type === DataType.Object) {
expect(decoded.name).toBe("OBJECT");
expect(decoded.alive).toBe(true);
}
@@ -294,7 +296,7 @@ describe("Frame.decodePosition", () => {
const frame = Frame.fromString(data);
const decoded = frame.decode() as PositionPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
});
it("decodes uncompressed position without timestamp", () => {
@@ -302,7 +304,7 @@ describe("Frame.decodePosition", () => {
const frame = Frame.fromString(data);
const decoded = frame.decode() as PositionPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.PositionNoTimestampNoMessaging);
});
it("handles ambiguity masking in position", () => {
@@ -322,7 +324,7 @@ describe("Frame.decodeStatus", () => {
structure: Dissected;
};
expect(res.payload).not.toBeNull();
if (res.payload?.type !== "status")
if (res.payload?.type !== DataType.Status)
throw new Error("expected status payload");
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
expect(payload.text).toBe("Testing status");
@@ -349,7 +351,7 @@ describe("Frame.decode (sections)", () => {
payload: PositionPayload | null;
structure: Dissected;
};
expect(result.payload?.type).toBe("position");
expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
});
});
@@ -456,29 +458,27 @@ describe("Frame.decodeMicE", () => {
const data =
"N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.MicECurrent);
if (decoded && decoded.type === "position") {
expect(decoded.messaging).toBe(true);
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position).toBeDefined();
expect(typeof decoded.position.latitude).toBe("number");
expect(typeof decoded.position.longitude).toBe("number");
expect(decoded.position.symbol).toBeDefined();
expect(decoded.micE).toBeDefined();
expect(decoded.micE?.messageType).toBeDefined();
expect(decoded.messageType).toBeDefined();
}
});
it("should decode a Mic-E packet with old format (single quote)", () => {
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.MicEOld);
});
});
@@ -486,11 +486,11 @@ describe("Frame.decodeMicE", () => {
it("should decode latitude from numeric digits (0-9)", () => {
const data = "CALL>123456:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3);
}
});
@@ -498,11 +498,11 @@ describe("Frame.decodeMicE", () => {
it("should decode latitude from letter digits (A-J)", () => {
const data = "CALL>ABC0EF:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3);
}
});
@@ -510,11 +510,11 @@ describe("Frame.decodeMicE", () => {
it("should decode latitude with mixed digits and letters", () => {
const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
}
});
@@ -522,11 +522,11 @@ describe("Frame.decodeMicE", () => {
it("should decode latitude for southern hemisphere", () => {
const data = "CALL>4A0P0U:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.latitude).toBeLessThan(0);
}
});
@@ -536,11 +536,11 @@ describe("Frame.decodeMicE", () => {
it("should decode longitude from information field", () => {
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(typeof decoded.position.longitude).toBe("number");
expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180);
expect(decoded.position.longitude).toBeLessThanOrEqual(180);
@@ -550,11 +550,11 @@ describe("Frame.decodeMicE", () => {
it("should handle eastern hemisphere longitude", () => {
const data = "CALL>4ABPDE:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(typeof decoded.position.longitude).toBe("number");
}
});
@@ -562,11 +562,11 @@ describe("Frame.decodeMicE", () => {
it("should handle longitude offset +100", () => {
const data = "CALL>4ABCDP:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(typeof decoded.position.longitude).toBe("number");
expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90);
}
@@ -581,7 +581,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
if (decoded.position.speed !== undefined) {
expect(decoded.position.speed).toBeGreaterThanOrEqual(0);
expect(typeof decoded.position.speed).toBe("number");
@@ -596,7 +596,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
if (decoded.position.course !== undefined) {
expect(decoded.position.course).toBeGreaterThanOrEqual(0);
expect(decoded.position.course).toBeLessThan(360);
@@ -611,7 +611,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.speed).toBeUndefined();
}
});
@@ -619,11 +619,11 @@ describe("Frame.decodeMicE", () => {
it("should not include zero or 360+ course in result", () => {
const data = "CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
if (decoded.position.course !== undefined) {
expect(decoded.position.course).toBeGreaterThan(0);
expect(decoded.position.course).toBeLessThan(360);
@@ -640,7 +640,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.symbol).toBeDefined();
expect(decoded.position.symbol?.table).toBeDefined();
expect(decoded.position.symbol?.code).toBeDefined();
@@ -658,7 +658,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
}
});
@@ -670,7 +670,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
if (decoded.position.comment?.startsWith("}")) {
expect(decoded.position.altitude).toBeDefined();
}
@@ -680,11 +680,11 @@ describe("Frame.decodeMicE", () => {
it("should prefer /A= format over base-91 when both present", () => {
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}}!!!/A=005000";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
}
});
@@ -692,11 +692,11 @@ describe("Frame.decodeMicE", () => {
it("should handle comment without altitude", () => {
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Just a comment";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.altitude).toBeUndefined();
expect(decoded.position.comment).toContain("Just a comment");
}
@@ -707,39 +707,34 @@ describe("Frame.decodeMicE", () => {
it("should decode message type M0 (Off Duty)", () => {
const data = "CALL>012345:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
expect(decoded.micE?.messageType).toBe("M0: Off Duty");
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.messageType).toBe("M0: Off Duty");
}
});
it("should decode message type M7 (Emergency)", () => {
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
expect(decoded.micE?.messageType).toBeDefined();
expect(typeof decoded.micE?.messageType).toBe("string");
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.messageType).toBeDefined();
expect(typeof decoded.messageType).toBe("string");
}
});
it("should decode standard vs custom message indicator", () => {
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
expect(decoded.micE?.isStandard).toBeDefined();
expect(typeof decoded.micE?.isStandard).toBe("boolean");
}
});
});
@@ -751,7 +746,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.comment).toContain("This is a test comment");
}
});
@@ -763,7 +758,7 @@ describe("Frame.decodeMicE", () => {
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.comment).toBeDefined();
}
});
@@ -799,8 +794,10 @@ describe("Frame.decodeMicE", () => {
const frame = Frame.fromString(data);
expect(() => frame.decode()).not.toThrow();
const decoded = frame.decode() as Payload;
expect(decoded === null || decoded?.type === "position").toBe(true);
const decoded = frame.decode() as MicEPayload;
expect(decoded === null || decoded?.type === DataType.MicECurrent).toBe(
true,
);
});
});
@@ -809,37 +806,21 @@ describe("Frame.decodeMicE", () => {
const data =
"N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
const decoded = frame.decode() as MicEPayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("position");
expect(decoded?.type).toBe(DataType.MicECurrent);
if (decoded && decoded.type === "position") {
expect(decoded.messaging).toBe(true);
if (decoded && decoded.type === DataType.MicECurrent) {
expect(decoded.position.latitude).toBeDefined();
expect(decoded.position.longitude).toBeDefined();
expect(decoded.position.symbol).toBeDefined();
expect(decoded.micE).toBeDefined();
expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90);
expect(Math.abs(decoded.position.longitude)).toBeLessThanOrEqual(180);
}
});
});
describe("Messaging capability", () => {
it("should always set messaging to true for Mic-E", () => {
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
const frame = Frame.fromString(data);
const decoded = frame.decode() as Payload;
expect(decoded).not.toBeNull();
if (decoded && decoded.type === "position") {
expect(decoded.messaging).toBe(true);
}
});
});
});
describe("Packet dissection with sections", () => {
@@ -878,12 +859,12 @@ describe("Packet dissection with sections", () => {
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
const frame = Frame.fromString(data);
const result = frame.decode(true) as {
payload: Payload;
payload: PositionPayload;
structure: Dissected;
};
expect(result.payload).not.toBeNull();
expect(result.payload?.type).toBe("position");
expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
expect(result.structure).toBeDefined();
expect(result.structure?.length).toBeGreaterThan(0);
@@ -900,10 +881,10 @@ describe("Packet dissection with sections", () => {
it("should not emit sections when emitSections is false or omitted", () => {
const data = "CALL>APRS:!4903.50N/07201.75W-Test";
const frame = Frame.fromString(data);
const result = frame.decode() as Payload;
const result = frame.decode() as PositionPayload;
expect(result).not.toBeNull();
expect(result?.type).toBe("position");
expect(result?.type).toBe(DataType.PositionNoTimestampNoMessaging);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((result as any).sections).toBeUndefined();
});
@@ -912,11 +893,13 @@ describe("Packet dissection with sections", () => {
const data = "CALL>APRS:@092345z4903.50N/07201.75W>";
const frame = Frame.fromString(data);
const result = frame.decode(true) as {
payload: Payload;
payload: PositionPayload;
structure: Dissected;
};
expect(result.payload?.type).toBe("position");
expect(result.payload?.type).toBe(
DataType.PositionWithTimestampWithMessaging,
);
const timestampSection = result.structure?.find(
(s) => s.name === "timestamp",
@@ -935,11 +918,13 @@ describe("Packet dissection with sections", () => {
const data = 'NOCALL-1>APRS:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
const frame = Frame.fromString(data);
const result = frame.decode(true) as {
payload: Payload;
payload: PositionPayload;
structure: Dissected;
};
expect(result.payload?.type).toBe("position");
expect(result.payload?.type).toBe(
DataType.PositionWithTimestampWithMessaging,
);
const positionSection = result.structure?.find(
(s) => s.name === "position",
@@ -956,11 +941,11 @@ describe("Packet dissection with sections", () => {
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
const frame = Frame.fromString(data);
const result = frame.decode(true) as {
payload: Payload;
payload: PositionPayload;
structure: Dissected;
};
expect(result.payload?.type).toBe("position");
expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
const commentSection = result.structure?.find((s) => s.name === "comment");
expect(commentSection).toBeDefined();
expect(commentSection?.data?.byteLength).toBe("Test message".length);
@@ -975,7 +960,7 @@ describe("Frame.decodeMessage", () => {
const decoded = frame.decode() as MessagePayload;
expect(decoded).not.toBeNull();
expect(decoded?.type).toBe("message");
expect(decoded?.type).toBe(DataType.Message);
expect(decoded?.addressee).toBe("KB1ABC-5");
expect(decoded?.text).toBe("Hello World");
});
@@ -989,7 +974,7 @@ describe("Frame.decodeMessage", () => {
};
expect(res.payload).not.toBeNull();
expect(res.payload.type).toBe("message");
expect(res.payload.type).toBe(DataType.Message);
const recipientSection = res.structure.find((s) => s.name === "recipient");
const textSection = res.structure.find((s) => s.name === "text");
expect(recipientSection).toBeDefined();
@@ -1013,7 +998,7 @@ describe("Frame.decoding: object and status", () => {
expect(res).toHaveProperty("payload");
expect(res.payload).not.toBeNull();
if (res.payload?.type !== "object")
if (res.payload?.type !== DataType.Object)
throw new Error("expected object payload");
const payload = res.payload as ObjectPayload & { timestamp?: ITimestamp };
@@ -1044,12 +1029,12 @@ describe("Frame.decoding: object and status", () => {
expect(res).toHaveProperty("payload");
expect(res.payload).not.toBeNull();
if (res.payload?.type !== "status")
if (res.payload?.type !== DataType.Status)
throw new Error("expected status payload");
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
expect(payload.type).toBe("status");
expect(payload.type).toBe(DataType.Status);
expect(payload.timestamp).toBeDefined();
expect(payload.timestamp?.day).toBe(12);