import type { Dissected, Field, Segment } from "@hamradio/packet"; import { describe, expect, it } from "vitest"; import { Frame } from "../src/frame"; import type { PositionPayload } from "../src/frame.types"; import { feetToMeters, milesToMeters } from "../src/parser"; describe("APRS extras test vectors", () => { it("parses PHG from position with messaging (spec vector 1)", () => { const raw = "NOCALL>APZRAZ,qAS,PA2RDK-14:=5154.19N/00627.77E>PHG500073 de NOCALL"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload } = res; expect(payload).not.toBeNull(); expect(payload!.position.phg).toBeDefined(); // PHG500073 parsed per spec: p=5 -> 25 W, h='0' -> 10 ft, g='0' -> 0 dBi expect(payload!.position.phg!.power).toBe(25); expect(payload!.position.phg!.height).toBeCloseTo(3.048, 3); expect(payload!.position.phg!.gain).toBe(0); }); it("parses PHG token with hyphen separators (spec vector 2)", () => { const raw = "NOCALL>APRS,TCPIP*,qAC,NINTH:;P-PA3RD *061000z5156.26NP00603.29E#PHG0210DAPNET"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload, structure } = res; expect(payload).not.toBeNull(); // Use a spec PHG example: PHG0210 -> p=0 -> power 0 W, h=2 -> 40 ft expect(payload!.position.phg).toBeDefined(); expect(payload!.position.phg!.power).toBe(0); expect(payload!.position.phg!.height).toBeCloseTo(12.192, 3); const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; expect(commentSeg).toBeDefined(); const fields = (commentSeg!.fields ?? []) as Field[]; const hasPHG = fields.some((f) => f.name === "PHG marker"); expect(hasPHG).toBe(true); }); it("parses DFS token with long numeric strength", () => { const raw = "N0CALL>APRS,WIDE1-1:!4500.00N/07000.00W#DFS2360/Your Comment"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload, structure } = res; expect(payload).not.toBeNull(); expect(payload!.position.dfs).toBeDefined(); // DFSshgd: strength is single-digit s value (here '2') expect(payload!.position.dfs!.strength).toBe(2); const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; expect(commentSeg).toBeDefined(); const fieldsDFS = (commentSeg!.fields ?? []) as Field[]; const hasDFS = fieldsDFS.some((f) => f.name === "DFS marker"); expect(hasDFS).toBe(true); }); it("parses course/speed in DDD/SSS form and altitude /A=", () => { const raw = "N0CALL>APRS,WIDE1-1:!4500.00N/07000.00W>090/045/A=001234"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload, structure } = res; expect(payload).not.toBeNull(); expect(payload!.position.course).toBe(90); // Speed is converted from knots to km/h expect(payload!.position.speed).toBeCloseTo(45 * 1.852, 3); // Altitude 001234 ft -> meters expect(Math.round((payload!.position.altitude || 0) / 0.3048)).toBe(1234); const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; expect(commentSeg).toBeDefined(); const fieldsCSE = (commentSeg!.fields ?? []) as Field[]; const hasCSE = fieldsCSE.some((f) => f.name === "course"); expect(hasCSE).toBe(true); }); it("parses combined tokens: DDD/SSS PHG and DFS", () => { const raw = "N0CALL>APRS,WIDE1-1:!4500.00N/07000.00W>090/045PHG5132DFS2132"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload, structure } = res; expect(payload).not.toBeNull(); expect(payload!.position.course).toBe(90); expect(payload!.position.speed).toBeCloseTo(45 * 1.852, 3); expect(payload!.position.phg).toBeDefined(); expect(payload!.position.dfs).toBeDefined(); expect(payload!.position.dfs!.strength).toBe(2); const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; expect(commentSeg).toBeDefined(); const fieldsCombined = (commentSeg!.fields ?? []) as Field[]; expect(fieldsCombined.some((f) => ["course", "PHG marker", "DFS marker"].includes(String(f.name)))).toBe(true); }); it("parses RNG token and emits structure", () => { const raw = "NOCALL-S>APDG01,TCPIP*,qAC,NOCALL-GS:;DN9PJF B *181227z5148.38ND00634.32EaRNG0001/A=000010 70cm Voice (D-Star) 439.50000MHz -7.6000MHz"; const frame = Frame.fromString(raw); const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected }; const { payload, structure } = res; expect(payload).not.toBeNull(); expect(payload!.position.altitude).toBeCloseTo(feetToMeters(10), 3); expect(payload!.position.range).toBe(milesToMeters(1) / 1000); const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; expect(commentSeg).toBeDefined(); const fieldsRNG = (commentSeg!.fields ?? []) as Field[]; const hasRNG = fieldsRNG.some((f) => f.name === "RNG marker"); expect(hasRNG).toBe(true); }); });