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,26 +1,28 @@
import type { Dissected, Segment, Field } from "@hamradio/packet";
import { FieldType } from "@hamradio/packet";
import type {
IAddress,
IFrame,
Payload,
ITimestamp,
PositionPayload,
MessagePayload,
IPosition,
ObjectPayload,
ItemPayload,
StatusPayload,
QueryPayload,
TelemetryDataPayload,
TelemetryBitSensePayload,
TelemetryCoefficientsPayload,
TelemetryParameterPayload,
TelemetryUnitPayload,
WeatherPayload,
RawGPSPayload,
StationCapabilitiesPayload,
ThirdPartyPayload,
import {
type IAddress,
type IFrame,
type Payload,
type ITimestamp,
type PositionPayload,
type MessagePayload,
type IPosition,
type ObjectPayload,
type ItemPayload,
type StatusPayload,
type QueryPayload,
type TelemetryDataPayload,
type TelemetryBitSensePayload,
type TelemetryCoefficientsPayload,
type TelemetryParameterPayload,
type TelemetryUnitPayload,
type WeatherPayload,
type RawGPSPayload,
type StationCapabilitiesPayload,
type ThirdPartyPayload,
DataType,
MicEPayload,
} from "./frame.types";
import { Position } from "./position";
import { base91ToNumber } from "./parser";
@@ -498,8 +500,30 @@ export class Frame implements IFrame {
}
}
let payloadType:
| DataType.PositionNoTimestampNoMessaging
| DataType.PositionNoTimestampWithMessaging
| DataType.PositionWithTimestampNoMessaging
| DataType.PositionWithTimestampWithMessaging;
switch (dataType) {
case "!":
payloadType = DataType.PositionNoTimestampNoMessaging;
break;
case "=":
payloadType = DataType.PositionNoTimestampWithMessaging;
break;
case "/":
payloadType = DataType.PositionWithTimestampNoMessaging;
break;
case "@":
payloadType = DataType.PositionWithTimestampWithMessaging;
break;
default:
return { payload: null };
}
const payload: PositionPayload = {
type: "position",
type: payloadType,
timestamp,
position,
messaging,
@@ -897,8 +921,20 @@ export class Frame implements IFrame {
}
}
const result: PositionPayload = {
type: "position",
let payloadType: DataType.MicECurrent | DataType.MicEOld;
switch (this.payload.charAt(0)) {
case "`":
payloadType = DataType.MicECurrent;
break;
case "'":
payloadType = DataType.MicEOld;
break;
default:
return { payload: null };
}
const result: MicEPayload = {
type: payloadType,
position: {
latitude,
longitude,
@@ -907,11 +943,8 @@ export class Frame implements IFrame {
code: symbolCode,
},
},
messaging: true, // Mic-E is always messaging-capable
micE: {
messageType,
isStandard,
},
messageType,
isStandard,
};
if (speed > 0) {
@@ -1133,6 +1166,13 @@ export class Frame implements IFrame {
text = this.payload.substring(textStart + 1);
}
const payload: MessagePayload = {
type: DataType.Message,
variant: "message",
addressee: recipient,
text,
};
if (withStructure) {
// Emit text section
segments.push({
@@ -1142,21 +1182,9 @@ export class Frame implements IFrame {
fields: [{ type: FieldType.STRING, name: "text", length: text.length }],
});
const payload: MessagePayload = {
type: "message",
addressee: recipient,
text,
};
return { payload, segment: segments };
}
const payload: MessagePayload = {
type: "message",
addressee: recipient,
text,
};
return { payload };
}
@@ -1285,7 +1313,7 @@ export class Frame implements IFrame {
}
const payload: ObjectPayload = {
type: "object",
type: DataType.Object,
name,
timestamp,
alive,
@@ -1420,7 +1448,7 @@ export class Frame implements IFrame {
}
const payload: ItemPayload = {
type: "item",
type: DataType.Item,
name,
alive,
position,
@@ -1472,7 +1500,7 @@ export class Frame implements IFrame {
}
const payload: StatusPayload = {
type: "status",
type: DataType.Status,
timestamp: undefined,
text: statusText,
};
@@ -1557,7 +1585,7 @@ export class Frame implements IFrame {
}
const payload: QueryPayload = {
type: "query",
type: DataType.Query,
queryType,
...(target ? { target } : {}),
};
@@ -1639,7 +1667,8 @@ export class Frame implements IFrame {
}
const payload: TelemetryDataPayload = {
type: "telemetry-data",
type: DataType.TelemetryData,
variant: "data",
sequence: isNaN(seq) ? 0 : seq,
analog,
digital: isNaN(digital) ? 0 : digital,
@@ -1664,7 +1693,8 @@ export class Frame implements IFrame {
});
}
const payload: TelemetryParameterPayload = {
type: "telemetry-parameters",
type: DataType.TelemetryData,
variant: "parameters",
names,
};
if (withStructure) return { payload, segment: segments };
@@ -1686,7 +1716,8 @@ export class Frame implements IFrame {
});
}
const payload: TelemetryUnitPayload = {
type: "telemetry-units",
type: DataType.TelemetryData,
variant: "unit",
units,
};
if (withStructure) return { payload, segment: segments };
@@ -1717,7 +1748,8 @@ export class Frame implements IFrame {
});
}
const payload: TelemetryCoefficientsPayload = {
type: "telemetry-coefficients",
type: DataType.TelemetryData,
variant: "coefficients",
coefficients,
};
if (withStructure) return { payload, segment: segments };
@@ -1741,7 +1773,8 @@ export class Frame implements IFrame {
});
}
const payload: TelemetryBitSensePayload = {
type: "telemetry-bitsense",
type: DataType.TelemetryData,
variant: "bitsense",
sense: isNaN(sense) ? 0 : sense,
...(projectName ? { projectName } : {}),
};
@@ -1818,7 +1851,9 @@ export class Frame implements IFrame {
const rest = this.payload.substring(offset).trim();
const payload: WeatherPayload = { type: "weather" };
const payload: WeatherPayload = {
type: DataType.WeatherReportNoPosition,
};
if (timestamp) payload.timestamp = timestamp;
if (position) payload.position = position;

View File

@@ -14,54 +14,51 @@ export interface IFrame {
}
// APRS Data Type Identifiers (first character of payload)
export const DataTypeIdentifier = {
export enum DataType {
// Position Reports
PositionNoTimestampNoMessaging: "!",
PositionNoTimestampWithMessaging: "=",
PositionWithTimestampNoMessaging: "/",
PositionWithTimestampWithMessaging: "@",
PositionNoTimestampNoMessaging = "!",
PositionNoTimestampWithMessaging = "=",
PositionWithTimestampNoMessaging = "/",
PositionWithTimestampWithMessaging = "@",
// Mic-E
MicECurrent: "`",
MicEOld: "'",
MicECurrent = "`",
MicEOld = "'",
// Messages and Bulletins
Message: ":",
Message = ":",
// Objects and Items
Object: ";",
Item: ")",
Object = ";",
Item = ")",
// Status
Status: ">",
Status = ">",
// Query
Query: "?",
Query = "?",
// Telemetry
TelemetryData: "T",
TelemetryData = "T",
// Weather
WeatherReportNoPosition: "_",
WeatherReportNoPosition = "_",
// Raw GPS Data
RawGPS: "$",
RawGPS = "$",
// Station Capabilities
StationCapabilities: "<",
StationCapabilities = "<",
// User-Defined
UserDefined: "{",
UserDefined = "{",
// Third-Party Traffic
ThirdParty: "}",
ThirdParty = "}",
// Invalid/Test Data
InvalidOrTest: ",",
} as const;
export type DataTypeIdentifier =
(typeof DataTypeIdentifier)[keyof typeof DataTypeIdentifier];
InvalidOrTest = ",",
}
export interface ISymbol {
table: string; // Symbol table identifier
@@ -99,7 +96,11 @@ export interface ITimestamp {
// Position Report Payload
export interface PositionPayload {
type: "position";
type:
| DataType.PositionNoTimestampNoMessaging
| DataType.PositionNoTimestampWithMessaging
| DataType.PositionWithTimestampNoMessaging
| DataType.PositionWithTimestampWithMessaging;
timestamp?: ITimestamp;
position: IPosition;
messaging: boolean; // Whether APRS messaging is enabled
@@ -128,19 +129,20 @@ export interface CompressedPosition {
// Mic-E Payload (compressed in destination address)
export interface MicEPayload {
type: "mic-e";
type: DataType.MicECurrent | DataType.MicEOld;
position: IPosition;
course?: number;
speed?: number;
altitude?: number;
messageType?: string; // Standard Mic-E message
isStandard?: boolean; // Whether messageType is a standard Mic-E message
telemetry?: number[]; // Optional telemetry channels
status?: string;
}
export type MessageVariant = "message" | "bulletin";
// Message Payload
export interface MessagePayload {
type: "message";
type: DataType.Message;
variant: "message";
addressee: string; // 9 character padded callsign
text: string; // Message text
messageNumber?: string; // Message ID for acknowledgment
@@ -150,7 +152,8 @@ export interface MessagePayload {
// Bulletin/Announcement (variant of message)
export interface BulletinPayload {
type: "bulletin";
type: DataType.Message;
variant: "bulletin";
bulletinId: string; // Bulletin identifier (BLN#)
text: string;
group?: string; // Optional group bulletin
@@ -158,7 +161,7 @@ export interface BulletinPayload {
// Object Payload
export interface ObjectPayload {
type: "object";
type: DataType.Object;
name: string; // 9 character object name
timestamp: ITimestamp;
alive: boolean; // True if object is active, false if killed
@@ -169,7 +172,7 @@ export interface ObjectPayload {
// Item Payload
export interface ItemPayload {
type: "item";
type: DataType.Item;
name: string; // 3-9 character item name
alive: boolean; // True if item is active, false if killed
position: IPosition;
@@ -177,7 +180,7 @@ export interface ItemPayload {
// Status Payload
export interface StatusPayload {
type: "status";
type: DataType.Status;
timestamp?: ITimestamp;
text: string;
maidenhead?: string; // Optional Maidenhead grid locator
@@ -189,14 +192,22 @@ export interface StatusPayload {
// Query Payload
export interface QueryPayload {
type: "query";
type: DataType.Query;
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
target?: string; // Target callsign or area
}
export type TelemetryVariant =
| "data"
| "parameters"
| "unit"
| "coefficients"
| "bitsense";
// Telemetry Data Payload
export interface TelemetryDataPayload {
type: "telemetry-data";
type: DataType.TelemetryData;
variant: "data";
sequence: number;
analog: number[]; // Up to 5 analog channels
digital: number; // 8-bit digital value
@@ -204,19 +215,22 @@ export interface TelemetryDataPayload {
// Telemetry Parameter Names
export interface TelemetryParameterPayload {
type: "telemetry-parameters";
type: DataType.TelemetryData;
variant: "parameters";
names: string[]; // Parameter names
}
// Telemetry Unit/Label
export interface TelemetryUnitPayload {
type: "telemetry-units";
type: DataType.TelemetryData;
variant: "unit";
units: string[]; // Units for each parameter
}
// Telemetry Coefficients
export interface TelemetryCoefficientsPayload {
type: "telemetry-coefficients";
type: DataType.TelemetryData;
variant: "coefficients";
coefficients: {
a: number[]; // a coefficients
b: number[]; // b coefficients
@@ -226,14 +240,15 @@ export interface TelemetryCoefficientsPayload {
// Telemetry Bit Sense/Project Name
export interface TelemetryBitSensePayload {
type: "telemetry-bitsense";
type: DataType.TelemetryData;
variant: "bitsense";
sense: number; // 8-bit sense value
projectName?: string;
}
// Weather Report Payload
export interface WeatherPayload {
type: "weather";
type: DataType.WeatherReportNoPosition;
timestamp?: ITimestamp;
position?: IPosition;
windDirection?: number; // Degrees
@@ -255,34 +270,33 @@ export interface WeatherPayload {
// Raw GPS Payload (NMEA sentences)
export interface RawGPSPayload {
type: "raw-gps";
type: DataType.RawGPS;
sentence: string; // Raw NMEA sentence
position?: IPosition; // Optional parsed position if available
}
// Station Capabilities Payload
export interface StationCapabilitiesPayload {
type: "capabilities";
type: DataType.StationCapabilities;
capabilities: string[];
}
// User-Defined Payload
export interface UserDefinedPayload {
type: "user-defined";
type: DataType.UserDefined;
userPacketType: string;
data: string;
}
// Third-Party Traffic Payload
export interface ThirdPartyPayload {
type: "third-party";
type: DataType.ThirdParty;
frame?: IFrame; // Optional nested frame if payload contains another APRS frame
comment?: string; // Optional comment
}
// DF Report Payload
export interface DFReportPayload {
type: "df-report";
timestamp?: ITimestamp;
position: IPosition;
course?: number;
@@ -295,7 +309,7 @@ export interface DFReportPayload {
}
export interface BasePayload {
type: string;
type: DataType;
}
// Union type for all decoded payload types
@@ -319,7 +333,6 @@ export type Payload = BasePayload &
| StationCapabilitiesPayload
| UserDefinedPayload
| ThirdPartyPayload
| DFReportPayload
);
// Extended Frame with decoded payload

View File

@@ -1,8 +1,13 @@
export { Frame, Address, Timestamp } from "./frame";
export { type IAddress, type IFrame, DataTypeIdentifier } from "./frame.types";
export {
type IAddress,
type IFrame,
DataType as DataTypeIdentifier,
} from "./frame.types";
export {
DataType,
type ISymbol,
type IPosition,
type ITimestamp,

View File

@@ -2,14 +2,14 @@ import { expect } from "vitest";
import { describe, it } from "vitest";
import { Dissected } from "@hamradio/packet";
import { Frame } from "../src/frame";
import { QueryPayload } from "../src/frame.types";
import { DataType, QueryPayload } from "../src/frame.types";
describe("Frame decode - Query", () => {
it("decodes simple query without target", () => {
const frame = Frame.fromString("SRC>DEST:?APRS");
const payload = frame.decode() as QueryPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("query");
expect(payload.type).toBe(DataType.Query);
expect(payload.queryType).toBe("APRS");
expect(payload.target).toBeUndefined();
});
@@ -18,7 +18,7 @@ describe("Frame decode - Query", () => {
const frame = Frame.fromString("SRC>DEST:?PING N0CALL");
const payload = frame.decode() as QueryPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("query");
expect(payload.type).toBe(DataType.Query);
expect(payload.queryType).toBe("PING");
expect(payload.target).toBe("N0CALL");
});
@@ -30,7 +30,7 @@ describe("Frame decode - Query", () => {
structure: Dissected;
};
expect(result).toHaveProperty("payload");
expect(result.payload.type).toBe("query");
expect(result.payload.type).toBe(DataType.Query);
expect(Array.isArray(result.structure)).toBe(true);
const names = result.structure.map((s) => s.name);
expect(names).toContain("query type");

View File

@@ -5,6 +5,7 @@ import {
TelemetryUnitPayload,
TelemetryCoefficientsPayload,
TelemetryBitSensePayload,
DataType,
} from "../src/frame.types";
import { Frame } from "../src/frame";
import { expect } from "vitest";
@@ -14,7 +15,8 @@ describe("Frame decode - Telemetry", () => {
const frame = Frame.fromString("SRC>DEST:T#1 10,20,30,40,50 7");
const payload = frame.decode() as TelemetryDataPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("telemetry-data");
expect(payload.type).toBe(DataType.TelemetryData);
expect(payload.variant).toBe("data");
expect(payload.sequence).toBe(1);
expect(Array.isArray(payload.analog)).toBe(true);
expect(payload.analog.length).toBe(5);
@@ -25,7 +27,8 @@ describe("Frame decode - Telemetry", () => {
const frame = Frame.fromString("SRC>DEST:TPARAM Temp,Hum,Wind");
const payload = frame.decode() as TelemetryParameterPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("telemetry-parameters");
expect(payload.type).toBe(DataType.TelemetryData);
expect(payload.variant).toBe("parameters");
expect(Array.isArray(payload.names)).toBe(true);
expect(payload.names).toEqual(["Temp", "Hum", "Wind"]);
});
@@ -34,7 +37,8 @@ describe("Frame decode - Telemetry", () => {
const frame = Frame.fromString("SRC>DEST:TUNIT C,% ,mph");
const payload = frame.decode() as TelemetryUnitPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("telemetry-units");
expect(payload.type).toBe(DataType.TelemetryData);
expect(payload.variant).toBe("unit");
expect(payload.units).toEqual(["C", "%", "mph"]);
});
@@ -42,7 +46,8 @@ describe("Frame decode - Telemetry", () => {
const frame = Frame.fromString("SRC>DEST:TCOEFF A:1,2 B:3,4 C:5,6");
const payload = frame.decode() as TelemetryCoefficientsPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("telemetry-coefficients");
expect(payload.type).toBe(DataType.TelemetryData);
expect(payload.variant).toBe("coefficients");
expect(payload.coefficients.a).toEqual([1, 2]);
expect(payload.coefficients.b).toEqual([3, 4]);
expect(payload.coefficients.c).toEqual([5, 6]);
@@ -52,7 +57,8 @@ describe("Frame decode - Telemetry", () => {
const frame = Frame.fromString("SRC>DEST:TBITS 255 ProjectX");
const payload = frame.decode() as TelemetryBitSensePayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("telemetry-bitsense");
expect(payload.type).toBe(DataType.TelemetryData);
expect(payload.variant).toBe("bitsense");
expect(payload.sense).toBe(255);
expect(payload.projectName).toBe("ProjectX");
});

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

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { Frame } from "../src/frame";
import { WeatherPayload } from "../src/frame.types";
import { DataType, WeatherPayload } from "../src/frame.types";
import { Dissected } from "@hamradio/packet";
describe("Frame decode - Weather", () => {
@@ -9,7 +9,7 @@ describe("Frame decode - Weather", () => {
const frame = Frame.fromString(data);
const payload = frame.decode() as WeatherPayload;
expect(payload).not.toBeNull();
expect(payload.type).toBe("weather");
expect(payload.type).toBe(DataType.WeatherReportNoPosition);
expect(payload.timestamp).toBeDefined();
expect(payload.windDirection).toBe(180);
expect(payload.windSpeed).toBe(10);