From c300aefc0bf530f21f33a818218821258ae47e9c Mon Sep 17 00:00:00 2001 From: maze Date: Sun, 15 Mar 2026 22:57:19 +0100 Subject: [PATCH] Major change: switched to DataType enum --- src/frame.ts | 137 +++++++++++++++--------- src/frame.types.ts | 107 ++++++++++--------- src/index.ts | 7 +- test/frame.query.test.ts | 8 +- test/frame.telemetry.test.ts | 16 ++- test/frame.test.ts | 197 ++++++++++++++++------------------- test/frame.weather.test.ts | 4 +- 7 files changed, 260 insertions(+), 216 deletions(-) diff --git a/src/frame.ts b/src/frame.ts index 792eb68..8175c97 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -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; diff --git a/src/frame.types.ts b/src/frame.types.ts index 3a1387e..f90b8d1 100644 --- a/src/frame.types.ts +++ b/src/frame.types.ts @@ -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 diff --git a/src/index.ts b/src/index.ts index 9407534..4de4029 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/test/frame.query.test.ts b/test/frame.query.test.ts index 56da05c..9a79682 100644 --- a/test/frame.query.test.ts +++ b/test/frame.query.test.ts @@ -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"); diff --git a/test/frame.telemetry.test.ts b/test/frame.telemetry.test.ts index 46074e9..ed8643d 100644 --- a/test/frame.telemetry.test.ts +++ b/test/frame.telemetry.test.ts @@ -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"); }); diff --git a/test/frame.test.ts b/test/frame.test.ts index ddfc480..43aa3e5 100644 --- a/test/frame.test.ts +++ b/test/frame.test.ts @@ -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); diff --git a/test/frame.weather.test.ts b/test/frame.weather.test.ts index a58ef0d..a7b5da6 100644 --- a/test/frame.weather.test.ts +++ b/test/frame.weather.test.ts @@ -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);