Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c28572e3b6
|
|||
|
17caa22331
|
|||
|
be8cd00c00
|
|||
|
7dc15e360d
|
|||
|
b1cd8449d9
|
|||
|
78dbd3b0ef
|
|||
|
df266bab12
|
|||
|
0ab62dab02
|
|||
|
38b617728c
|
19
.prettierrc.ts
Normal file
19
.prettierrc.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { type Config } from "prettier";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
plugins: ["@trivago/prettier-plugin-sort-imports"],
|
||||||
|
trailingComma: "none",
|
||||||
|
printWidth: 120,
|
||||||
|
importOrder: [
|
||||||
|
"<BUILTIN_MODULES>",
|
||||||
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"(?:services|components|contexts|pages|libs|types)/(.*)$",
|
||||||
|
"^[./].*\\.(?:ts|tsx)$",
|
||||||
|
"\\.(?:scss|css)$",
|
||||||
|
"^[./]"
|
||||||
|
],
|
||||||
|
importOrderSeparation: true,
|
||||||
|
importOrderSortSpecifiers: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
29
package.json
29
package.json
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hamradio/aprs",
|
"name": "@hamradio/aprs",
|
||||||
"version": "1.1.1",
|
"type": "module",
|
||||||
|
"version": "1.3.0",
|
||||||
"description": "APRS (Automatic Packet Reporting System) protocol support for Typescript",
|
"description": "APRS (Automatic Packet Reporting System) protocol support for Typescript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"APRS",
|
"APRS",
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Wijnand Modderman-Lenstra",
|
"author": "Wijnand Modderman-Lenstra",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.js"
|
"require": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,18 +39,20 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^10.0.1",
|
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
|
||||||
"eslint": "^10.0.3",
|
|
||||||
"globals": "^17.4.0",
|
|
||||||
"tsup": "^8.5.1",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"typescript-eslint": "^8.57.0",
|
|
||||||
"vitest": "^4.0.18"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hamradio/packet": "^1.1.0",
|
"@hamradio/packet": "^1.1.0",
|
||||||
"extended-nmea": "^2.1.3"
|
"extended-nmea": "^2.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
|
"eslint": "^10.0.3",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.57.1",
|
||||||
|
"vitest": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1088
src/deviceid.ts
Normal file
1088
src/deviceid.ts
Normal file
File diff suppressed because it is too large
Load Diff
1015
src/frame.ts
1015
src/frame.ts
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,12 @@
|
|||||||
import { Dissected, Segment } from "@hamradio/packet";
|
import { Dissected, Segment } from "@hamradio/packet";
|
||||||
|
|
||||||
|
// Any comment that contains this marker will set the doNotArchive flag on the
|
||||||
|
// decoded payload, which can be used by applications to skip archiving or
|
||||||
|
// logging frames that are meant to be transient or test data. This allows users
|
||||||
|
// to include the marker in their APRS comments when they want to indicate that
|
||||||
|
// a particular frame should not be stored long-term.
|
||||||
|
export const DO_NOT_ARCHIVE_MARKER = "!x!";
|
||||||
|
|
||||||
export interface IAddress {
|
export interface IAddress {
|
||||||
call: string;
|
call: string;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
@@ -22,7 +29,7 @@ export enum DataType {
|
|||||||
PositionWithTimestampWithMessaging = "@",
|
PositionWithTimestampWithMessaging = "@",
|
||||||
|
|
||||||
// Mic-E
|
// Mic-E
|
||||||
MicECurrent = "`",
|
MicE = "`",
|
||||||
MicEOld = "'",
|
MicEOld = "'",
|
||||||
|
|
||||||
// Messages and Bulletins
|
// Messages and Bulletins
|
||||||
@@ -57,9 +64,30 @@ export enum DataType {
|
|||||||
ThirdParty = "}",
|
ThirdParty = "}",
|
||||||
|
|
||||||
// Invalid/Test Data
|
// Invalid/Test Data
|
||||||
InvalidOrTest = ",",
|
InvalidOrTest = ","
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DataTypeNames: { [key in DataType]: string } = {
|
||||||
|
[DataType.PositionNoTimestampNoMessaging]: "position",
|
||||||
|
[DataType.PositionNoTimestampWithMessaging]: "position with messaging",
|
||||||
|
[DataType.PositionWithTimestampNoMessaging]: "position with timestamp",
|
||||||
|
[DataType.PositionWithTimestampWithMessaging]: "position with timestamp and messaging",
|
||||||
|
[DataType.MicE]: "Mic-E",
|
||||||
|
[DataType.MicEOld]: "Mic-E (old)",
|
||||||
|
[DataType.Message]: "message/bulletin",
|
||||||
|
[DataType.Object]: "object",
|
||||||
|
[DataType.Item]: "item",
|
||||||
|
[DataType.Status]: "status",
|
||||||
|
[DataType.Query]: "query",
|
||||||
|
[DataType.TelemetryData]: "telemetry data",
|
||||||
|
[DataType.WeatherReportNoPosition]: "weather report",
|
||||||
|
[DataType.RawGPS]: "raw GPS data",
|
||||||
|
[DataType.StationCapabilities]: "station capabilities",
|
||||||
|
[DataType.UserDefined]: "user defined",
|
||||||
|
[DataType.ThirdParty]: "third-party traffic",
|
||||||
|
[DataType.InvalidOrTest]: "invalid/test"
|
||||||
|
};
|
||||||
|
|
||||||
export interface ISymbol {
|
export interface ISymbol {
|
||||||
table: string; // Symbol table identifier
|
table: string; // Symbol table identifier
|
||||||
code: string; // Symbol code
|
code: string; // Symbol code
|
||||||
@@ -73,8 +101,11 @@ export interface IPosition {
|
|||||||
longitude: number; // Decimal degrees
|
longitude: number; // Decimal degrees
|
||||||
ambiguity?: number; // Position ambiguity (0-4)
|
ambiguity?: number; // Position ambiguity (0-4)
|
||||||
altitude?: number; // Meters
|
altitude?: number; // Meters
|
||||||
speed?: number; // Speed in knots/kmh depending on source
|
speed?: number; // Speed in km/h
|
||||||
course?: number; // Course in degrees
|
course?: number; // Course in degrees
|
||||||
|
range?: number; // Kilometers
|
||||||
|
phg?: IPowerHeightGain;
|
||||||
|
dfs?: IDirectionFinding;
|
||||||
symbol?: ISymbol;
|
symbol?: ISymbol;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
|
||||||
@@ -83,6 +114,22 @@ export interface IPosition {
|
|||||||
distanceTo?(other: IPosition): number; // Optional method to calculate distance to another position
|
distanceTo?(other: IPosition): number; // Optional method to calculate distance to another position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPowerHeightGain {
|
||||||
|
power?: number; // Transmit power in watts
|
||||||
|
height?: number; // Antenna height in meters
|
||||||
|
gain?: number; // Antenna gain in dBi
|
||||||
|
directivity?: number | "omni" | "unknown"; // Optional directivity pattern (numeric code or "omni")
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDirectionFinding {
|
||||||
|
bearing?: number; // Direction finding bearing in degrees
|
||||||
|
strength?: number; // Relative signal strength (0-9)
|
||||||
|
height?: number; // Antenna height in meters
|
||||||
|
gain?: number; // Antenna gain in dBi
|
||||||
|
quality?: number; // Signal quality or other metric (0-9)
|
||||||
|
directivity?: number | "omni" | "unknown"; // Optional directivity pattern (numeric code or "omni")
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITimestamp {
|
export interface ITimestamp {
|
||||||
day?: number; // Day of month (DHM format)
|
day?: number; // Day of month (DHM format)
|
||||||
month?: number; // Month (MDHM format)
|
month?: number; // Month (MDHM format)
|
||||||
@@ -101,6 +148,7 @@ export interface PositionPayload {
|
|||||||
| DataType.PositionNoTimestampWithMessaging
|
| DataType.PositionNoTimestampWithMessaging
|
||||||
| DataType.PositionWithTimestampNoMessaging
|
| DataType.PositionWithTimestampNoMessaging
|
||||||
| DataType.PositionWithTimestampWithMessaging;
|
| DataType.PositionWithTimestampWithMessaging;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
messaging: boolean; // Whether APRS messaging is enabled
|
messaging: boolean; // Whether APRS messaging is enabled
|
||||||
@@ -129,7 +177,8 @@ export interface CompressedPosition {
|
|||||||
|
|
||||||
// Mic-E Payload (compressed in destination address)
|
// Mic-E Payload (compressed in destination address)
|
||||||
export interface MicEPayload {
|
export interface MicEPayload {
|
||||||
type: DataType.MicECurrent | DataType.MicEOld;
|
type: DataType.MicE | DataType.MicEOld;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
messageType?: string; // Standard Mic-E message
|
messageType?: string; // Standard Mic-E message
|
||||||
isStandard?: boolean; // Whether messageType is a standard Mic-E message
|
isStandard?: boolean; // Whether messageType is a standard Mic-E message
|
||||||
@@ -143,6 +192,7 @@ export type MessageVariant = "message" | "bulletin";
|
|||||||
export interface MessagePayload {
|
export interface MessagePayload {
|
||||||
type: DataType.Message;
|
type: DataType.Message;
|
||||||
variant: "message";
|
variant: "message";
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
addressee: string; // 9 character padded callsign
|
addressee: string; // 9 character padded callsign
|
||||||
text: string; // Message text
|
text: string; // Message text
|
||||||
messageNumber?: string; // Message ID for acknowledgment
|
messageNumber?: string; // Message ID for acknowledgment
|
||||||
@@ -154,6 +204,7 @@ export interface MessagePayload {
|
|||||||
export interface BulletinPayload {
|
export interface BulletinPayload {
|
||||||
type: DataType.Message;
|
type: DataType.Message;
|
||||||
variant: "bulletin";
|
variant: "bulletin";
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
bulletinId: string; // Bulletin identifier (BLN#)
|
bulletinId: string; // Bulletin identifier (BLN#)
|
||||||
text: string;
|
text: string;
|
||||||
group?: string; // Optional group bulletin
|
group?: string; // Optional group bulletin
|
||||||
@@ -162,6 +213,7 @@ export interface BulletinPayload {
|
|||||||
// Object Payload
|
// Object Payload
|
||||||
export interface ObjectPayload {
|
export interface ObjectPayload {
|
||||||
type: DataType.Object;
|
type: DataType.Object;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
name: string; // 9 character object name
|
name: string; // 9 character object name
|
||||||
timestamp: ITimestamp;
|
timestamp: ITimestamp;
|
||||||
alive: boolean; // True if object is active, false if killed
|
alive: boolean; // True if object is active, false if killed
|
||||||
@@ -173,6 +225,7 @@ export interface ObjectPayload {
|
|||||||
// Item Payload
|
// Item Payload
|
||||||
export interface ItemPayload {
|
export interface ItemPayload {
|
||||||
type: DataType.Item;
|
type: DataType.Item;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
name: string; // 3-9 character item name
|
name: string; // 3-9 character item name
|
||||||
alive: boolean; // True if item is active, false if killed
|
alive: boolean; // True if item is active, false if killed
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
@@ -181,6 +234,7 @@ export interface ItemPayload {
|
|||||||
// Status Payload
|
// Status Payload
|
||||||
export interface StatusPayload {
|
export interface StatusPayload {
|
||||||
type: DataType.Status;
|
type: DataType.Status;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
text: string;
|
text: string;
|
||||||
maidenhead?: string; // Optional Maidenhead grid locator
|
maidenhead?: string; // Optional Maidenhead grid locator
|
||||||
@@ -197,12 +251,7 @@ export interface QueryPayload {
|
|||||||
target?: string; // Target callsign or area
|
target?: string; // Target callsign or area
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TelemetryVariant =
|
export type TelemetryVariant = "data" | "parameters" | "unit" | "coefficients" | "bitsense";
|
||||||
| "data"
|
|
||||||
| "parameters"
|
|
||||||
| "unit"
|
|
||||||
| "coefficients"
|
|
||||||
| "bitsense";
|
|
||||||
|
|
||||||
// Telemetry Data Payload
|
// Telemetry Data Payload
|
||||||
export interface TelemetryDataPayload {
|
export interface TelemetryDataPayload {
|
||||||
@@ -310,6 +359,7 @@ export interface DFReportPayload {
|
|||||||
|
|
||||||
export interface BasePayload {
|
export interface BasePayload {
|
||||||
type: DataType;
|
type: DataType;
|
||||||
|
doNotArchive?: boolean; // Optional flag to indicate frame should not be archived
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union type for all decoded payload types
|
// Union type for all decoded payload types
|
||||||
|
|||||||
15
src/index.ts
15
src/index.ts
@@ -1,16 +1,14 @@
|
|||||||
export { Frame, Address, Timestamp } from "./frame";
|
export { Frame, Address, Timestamp } from "./frame";
|
||||||
|
|
||||||
export {
|
export { type IAddress, type IFrame, DataType as DataTypeIdentifier } from "./frame.types";
|
||||||
type IAddress,
|
|
||||||
type IFrame,
|
|
||||||
DataType as DataTypeIdentifier,
|
|
||||||
} from "./frame.types";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DataType,
|
DataType,
|
||||||
type ISymbol,
|
type ISymbol,
|
||||||
type IPosition,
|
type IPosition,
|
||||||
type ITimestamp,
|
type ITimestamp,
|
||||||
|
type IPowerHeightGain,
|
||||||
|
type IDirectionFinding,
|
||||||
type PositionPayload,
|
type PositionPayload,
|
||||||
type CompressedPosition,
|
type CompressedPosition,
|
||||||
type MicEPayload,
|
type MicEPayload,
|
||||||
@@ -33,7 +31,7 @@ export {
|
|||||||
type DFReportPayload,
|
type DFReportPayload,
|
||||||
type BasePayload,
|
type BasePayload,
|
||||||
type Payload,
|
type Payload,
|
||||||
type DecodedFrame,
|
type DecodedFrame
|
||||||
} from "./frame.types";
|
} from "./frame.types";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -43,5 +41,8 @@ export {
|
|||||||
feetToMeters,
|
feetToMeters,
|
||||||
metersToFeet,
|
metersToFeet,
|
||||||
celsiusToFahrenheit,
|
celsiusToFahrenheit,
|
||||||
fahrenheitToCelsius,
|
fahrenheitToCelsius
|
||||||
} from "./parser";
|
} from "./parser";
|
||||||
|
|
||||||
|
export { getDeviceID } from "./deviceid";
|
||||||
|
export type { DeviceID } from "./deviceid";
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ export const base91ToNumber = (str: string): number => {
|
|||||||
const digit = charCode - 33; // Base91 uses chars 33-123 (! to {)
|
const digit = charCode - 33; // Base91 uses chars 33-123 (! to {)
|
||||||
|
|
||||||
if (digit < 0 || digit >= base) {
|
if (digit < 0 || digit >= base) {
|
||||||
throw new Error(
|
throw new Error(`Invalid Base91 character: '${str[i]}' (code ${charCode})`);
|
||||||
`Invalid Base91 character: '${str[i]}' (code ${charCode})`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value = value * base + digit;
|
value = value * base + digit;
|
||||||
@@ -62,6 +60,15 @@ export const feetToMeters = (feet: number): number => {
|
|||||||
return feet * FEET_TO_METERS;
|
return feet * FEET_TO_METERS;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert miles to meters.
|
||||||
|
* @param miles number of miles
|
||||||
|
* @returns meters
|
||||||
|
*/
|
||||||
|
export const milesToMeters = (miles: number): number => {
|
||||||
|
return miles * 1609.344;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert altitude from meters to feet.
|
* Convert altitude from meters to feet.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IPosition, ISymbol } from "./frame.types";
|
import { IDirectionFinding, IPosition, IPowerHeightGain, ISymbol } from "./frame.types";
|
||||||
|
|
||||||
export class Symbol implements ISymbol {
|
export class Symbol implements ISymbol {
|
||||||
table: string; // Symbol table identifier
|
table: string; // Symbol table identifier
|
||||||
@@ -10,9 +10,7 @@ export class Symbol implements ISymbol {
|
|||||||
this.code = table[1];
|
this.code = table[1];
|
||||||
this.table = table[0];
|
this.table = table[0];
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`);
|
||||||
`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.table = table;
|
this.table = table;
|
||||||
@@ -34,6 +32,9 @@ export class Position implements IPosition {
|
|||||||
course?: number; // Course in degrees
|
course?: number; // Course in degrees
|
||||||
symbol?: Symbol;
|
symbol?: Symbol;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
range?: number;
|
||||||
|
phg?: IPowerHeightGain;
|
||||||
|
dfs?: IDirectionFinding;
|
||||||
|
|
||||||
constructor(data: Partial<IPosition>) {
|
constructor(data: Partial<IPosition>) {
|
||||||
this.latitude = data.latitude ?? 0;
|
this.latitude = data.latitude ?? 0;
|
||||||
@@ -48,6 +49,9 @@ export class Position implements IPosition {
|
|||||||
this.symbol = new Symbol(data.symbol.table, data.symbol.code);
|
this.symbol = new Symbol(data.symbol.table, data.symbol.code);
|
||||||
}
|
}
|
||||||
this.comment = data.comment;
|
this.comment = data.comment;
|
||||||
|
this.range = data.range;
|
||||||
|
this.phg = data.phg;
|
||||||
|
this.dfs = data.dfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
|
|||||||
22
test/deviceid.test.ts
Normal file
22
test/deviceid.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getDeviceID } from "../src/deviceid";
|
||||||
|
import { Frame } from "../src/frame";
|
||||||
|
|
||||||
|
describe("DeviceID parsing", () => {
|
||||||
|
it("parses known device ID from tocall", () => {
|
||||||
|
const data = "WB2OSZ-5>APDW17:!4237.14NS07120.83W#PHG7140";
|
||||||
|
const frame = Frame.fromString(data);
|
||||||
|
const deviceID = getDeviceID(frame.destination);
|
||||||
|
expect(deviceID).not.toBeNull();
|
||||||
|
expect(deviceID?.tocall).toBe("APDW??");
|
||||||
|
expect(deviceID?.vendor).toBe("WB2OSZ");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for unknown device ID", () => {
|
||||||
|
const data = "CALL>WORLD:!4237.14NS07120.83W#PHG7140";
|
||||||
|
const frame = Frame.fromString(data);
|
||||||
|
const deviceID = getDeviceID(frame.destination);
|
||||||
|
expect(deviceID).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { Frame } from "../src/frame";
|
|
||||||
import type { Payload, StationCapabilitiesPayload } from "../src/frame.types";
|
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { Frame } from "../src/frame";
|
||||||
|
import { DataType, type Payload, type StationCapabilitiesPayload } from "../src/frame.types";
|
||||||
|
|
||||||
describe("Frame.decodeCapabilities", () => {
|
describe("Frame.decodeCapabilities", () => {
|
||||||
it("parses comma separated capabilities", () => {
|
it("parses comma separated capabilities", () => {
|
||||||
@@ -9,7 +10,7 @@ describe("Frame.decodeCapabilities", () => {
|
|||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as StationCapabilitiesPayload;
|
const decoded = frame.decode() as StationCapabilitiesPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded.type).toBe("capabilities");
|
expect(decoded.type).toBe(DataType.StationCapabilities);
|
||||||
expect(Array.isArray(decoded.capabilities)).toBeTruthy();
|
expect(Array.isArray(decoded.capabilities)).toBeTruthy();
|
||||||
expect(decoded.capabilities).toContain("IGATE");
|
expect(decoded.capabilities).toContain("IGATE");
|
||||||
expect(decoded.capabilities).toContain("MSG_CNT");
|
expect(decoded.capabilities).toContain("MSG_CNT");
|
||||||
@@ -23,7 +24,7 @@ describe("Frame.decodeCapabilities", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
if (res.payload && res.payload.type !== "capabilities")
|
if (res.payload && res.payload.type !== DataType.StationCapabilities)
|
||||||
throw new Error("expected capabilities payload");
|
throw new Error("expected capabilities payload");
|
||||||
expect(res.structure).toBeDefined();
|
expect(res.structure).toBeDefined();
|
||||||
const caps = res.structure.find((s) => s.name === "capabilities");
|
const caps = res.structure.find((s) => s.name === "capabilities");
|
||||||
|
|||||||
116
test/frame.extras.test.ts
Normal file
116
test/frame.extras.test.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Dissected } from "@hamradio/packet";
|
||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
import { Dissected } from "@hamradio/packet";
|
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import { DataType, QueryPayload } from "../src/frame.types";
|
import { DataType, QueryPayload } from "../src/frame.types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { Frame } from "../src/frame";
|
|
||||||
import type { RawGPSPayload } from "../src/frame.types";
|
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { Frame } from "../src/frame";
|
||||||
|
import { DataType, type RawGPSPayload } from "../src/frame.types";
|
||||||
|
|
||||||
describe("Raw GPS decoding", () => {
|
describe("Raw GPS decoding", () => {
|
||||||
it("decodes simple NMEA sentence as raw-gps payload", () => {
|
it("decodes simple NMEA sentence as raw-gps payload", () => {
|
||||||
const sentence =
|
const sentence = "GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||||
"GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
|
||||||
const frameStr = `SRC>DEST:$${sentence}`;
|
const frameStr = `SRC>DEST:$${sentence}`;
|
||||||
|
|
||||||
const f = Frame.parse(frameStr);
|
const f = Frame.parse(frameStr);
|
||||||
const payload = f.decode(false) as RawGPSPayload | null;
|
const payload = f.decode(false) as RawGPSPayload | null;
|
||||||
|
|
||||||
expect(payload).not.toBeNull();
|
expect(payload).not.toBeNull();
|
||||||
expect(payload?.type).toBe("raw-gps");
|
expect(payload?.type).toBe(DataType.RawGPS);
|
||||||
expect(payload?.sentence).toBe(sentence);
|
expect(payload?.sentence).toBe(sentence);
|
||||||
expect(payload?.position).toBeDefined();
|
expect(payload?.position).toBeDefined();
|
||||||
expect(typeof payload?.position?.latitude).toBe("number");
|
expect(typeof payload?.position?.latitude).toBe("number");
|
||||||
@@ -21,8 +21,7 @@ describe("Raw GPS decoding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns structure when requested", () => {
|
it("returns structure when requested", () => {
|
||||||
const sentence =
|
const sentence = "GPGGA,092750.000,5321.6802,N,00630.3372,W,1,08,1.0,73.0,M,0.0,M,,*6A";
|
||||||
"GPGGA,092750.000,5321.6802,N,00630.3372,W,1,08,1.0,73.0,M,0.0,M,,*6A";
|
|
||||||
const frameStr = `SRC>DEST:$${sentence}`;
|
const frameStr = `SRC>DEST:$${sentence}`;
|
||||||
|
|
||||||
const f = Frame.parse(frameStr);
|
const f = Frame.parse(frameStr);
|
||||||
@@ -32,7 +31,7 @@ describe("Raw GPS decoding", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload).not.toBeNull();
|
expect(result.payload).not.toBeNull();
|
||||||
expect(result.payload?.type).toBe("raw-gps");
|
expect(result.payload?.type).toBe(DataType.RawGPS);
|
||||||
expect(result.payload?.sentence).toBe(sentence);
|
expect(result.payload?.sentence).toBe(sentence);
|
||||||
expect(result.payload?.position).toBeDefined();
|
expect(result.payload?.position).toBeDefined();
|
||||||
expect(typeof result.payload?.position?.latitude).toBe("number");
|
expect(typeof result.payload?.position?.latitude).toBe("number");
|
||||||
@@ -40,9 +39,7 @@ describe("Raw GPS decoding", () => {
|
|||||||
expect(result.structure).toBeDefined();
|
expect(result.structure).toBeDefined();
|
||||||
const rawSection = result.structure.find((s) => s.name === "raw-gps");
|
const rawSection = result.structure.find((s) => s.name === "raw-gps");
|
||||||
expect(rawSection).toBeDefined();
|
expect(rawSection).toBeDefined();
|
||||||
const posSection = result.structure.find(
|
const posSection = result.structure.find((s) => s.name === "raw-gps-position");
|
||||||
(s) => s.name === "raw-gps-position",
|
|
||||||
);
|
|
||||||
expect(posSection).toBeDefined();
|
expect(posSection).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
|
import { expect } from "vitest";
|
||||||
|
|
||||||
|
import { Frame } from "../src/frame";
|
||||||
import {
|
import {
|
||||||
|
DataType,
|
||||||
|
TelemetryBitSensePayload,
|
||||||
|
TelemetryCoefficientsPayload,
|
||||||
TelemetryDataPayload,
|
TelemetryDataPayload,
|
||||||
TelemetryParameterPayload,
|
TelemetryParameterPayload,
|
||||||
TelemetryUnitPayload,
|
TelemetryUnitPayload
|
||||||
TelemetryCoefficientsPayload,
|
|
||||||
TelemetryBitSensePayload,
|
|
||||||
DataType,
|
|
||||||
} from "../src/frame.types";
|
} from "../src/frame.types";
|
||||||
import { Frame } from "../src/frame";
|
|
||||||
import { expect } from "vitest";
|
|
||||||
|
|
||||||
describe("Frame decode - Telemetry", () => {
|
describe("Frame decode - Telemetry", () => {
|
||||||
it("decodes telemetry data payload", () => {
|
it("decodes telemetry data payload", () => {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import { Dissected, FieldType } from "@hamradio/packet";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { Address, Frame, Timestamp } from "../src/frame";
|
import { Address, Frame, Timestamp } from "../src/frame";
|
||||||
import {
|
import {
|
||||||
type Payload,
|
DataType,
|
||||||
type PositionPayload,
|
|
||||||
type ObjectPayload,
|
|
||||||
type StatusPayload,
|
|
||||||
type ITimestamp,
|
type ITimestamp,
|
||||||
type MessagePayload,
|
type MessagePayload,
|
||||||
DataType,
|
|
||||||
MicEPayload,
|
MicEPayload,
|
||||||
|
type ObjectPayload,
|
||||||
|
type Payload,
|
||||||
|
type PositionPayload,
|
||||||
|
type StatusPayload
|
||||||
} from "../src/frame.types";
|
} from "../src/frame.types";
|
||||||
import { Dissected, FieldType } from "@hamradio/packet";
|
|
||||||
|
|
||||||
// Address parsing: split by method
|
// Address parsing: split by method
|
||||||
describe("Address.parse", () => {
|
describe("Address.parse", () => {
|
||||||
@@ -49,8 +50,7 @@ describe("Frame.constructor", () => {
|
|||||||
// Frame properties / instance methods
|
// Frame properties / instance methods
|
||||||
describe("Frame.getDataTypeIdentifier", () => {
|
describe("Frame.getDataTypeIdentifier", () => {
|
||||||
it("returns @ for position identifier", () => {
|
it("returns @ for position identifier", () => {
|
||||||
const data =
|
const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
expect(frame.getDataTypeIdentifier()).toBe("@");
|
expect(frame.getDataTypeIdentifier()).toBe("@");
|
||||||
});
|
});
|
||||||
@@ -76,8 +76,7 @@ describe("Frame.getDataTypeIdentifier", () => {
|
|||||||
|
|
||||||
describe("Frame.decode (basic)", () => {
|
describe("Frame.decode (basic)", () => {
|
||||||
it("should call decode and return position payload", () => {
|
it("should call decode and return position payload", () => {
|
||||||
const data =
|
const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as PositionPayload;
|
const decoded = frame.decode() as PositionPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
@@ -99,7 +98,7 @@ describe("Frame.decode (basic)", () => {
|
|||||||
{ data: "CALL>APRS:$GPRMC,...", type: "$" },
|
{ data: "CALL>APRS:$GPRMC,...", type: "$" },
|
||||||
{ data: "CALL>APRS:<IGATE,MSG_CNT", type: "<" },
|
{ data: "CALL>APRS:<IGATE,MSG_CNT", type: "<" },
|
||||||
{ data: "CALL>APRS:{01", type: "{" },
|
{ data: "CALL>APRS:{01", type: "{" },
|
||||||
{ data: "CALL>APRS:}W1AW>APRS:test", type: "}" },
|
{ data: "CALL>APRS:}W1AW>APRS:test", type: "}" }
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
const frame = Frame.fromString(testCase.data);
|
const frame = Frame.fromString(testCase.data);
|
||||||
@@ -112,53 +111,70 @@ describe("Frame.decode (basic)", () => {
|
|||||||
// Static functions
|
// Static functions
|
||||||
describe("Frame.fromString", () => {
|
describe("Frame.fromString", () => {
|
||||||
it("parses APRS position frame (test vector 1)", () => {
|
it("parses APRS position frame (test vector 1)", () => {
|
||||||
const data =
|
const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
|
||||||
const result = Frame.fromString(data);
|
const result = Frame.fromString(data);
|
||||||
expect(result.source).toEqual({
|
expect(result.source).toEqual({
|
||||||
call: "NOCALL",
|
call: "NOCALL",
|
||||||
ssid: "1",
|
ssid: "1",
|
||||||
isRepeated: false,
|
isRepeated: false
|
||||||
});
|
});
|
||||||
expect(result.destination).toEqual({
|
expect(result.destination).toEqual({
|
||||||
call: "APRS",
|
call: "APRS",
|
||||||
ssid: "",
|
ssid: "",
|
||||||
isRepeated: false,
|
isRepeated: false
|
||||||
});
|
});
|
||||||
expect(result.path).toHaveLength(1);
|
expect(result.path).toHaveLength(1);
|
||||||
expect(result.path[0]).toEqual({
|
expect(result.path[0]).toEqual({
|
||||||
call: "WIDE1",
|
call: "WIDE1",
|
||||||
ssid: "1",
|
ssid: "1",
|
||||||
isRepeated: false,
|
isRepeated: false
|
||||||
});
|
});
|
||||||
expect(result.payload).toBe('@092345z/:*E";qZ=OMRC/A=088132Hello World!');
|
expect(result.payload).toBe('@092345z/:*E";qZ=OMRC/A=088132Hello World!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses APRS position frame without messaging (test vector 3)", () => {
|
||||||
|
const data = "N0CALL-7>APLRT1,qAO,DG2EAZ-10:!/4CkRP&-V>76Q";
|
||||||
|
const result = Frame.fromString(data);
|
||||||
|
expect(result.source).toEqual({
|
||||||
|
call: "N0CALL",
|
||||||
|
ssid: "7",
|
||||||
|
isRepeated: false
|
||||||
|
});
|
||||||
|
expect(result.destination).toEqual({
|
||||||
|
call: "APLRT1",
|
||||||
|
ssid: "",
|
||||||
|
isRepeated: false
|
||||||
|
});
|
||||||
|
expect(result.path).toHaveLength(2);
|
||||||
|
|
||||||
|
const payload = result.decode(false);
|
||||||
|
expect(payload).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("parses APRS Mic-E frame with repeated digipeater (test vector 2)", () => {
|
it("parses APRS Mic-E frame with repeated digipeater (test vector 2)", () => {
|
||||||
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
||||||
const result = Frame.fromString(data);
|
const result = Frame.fromString(data);
|
||||||
expect(result.source).toEqual({
|
expect(result.source).toEqual({
|
||||||
call: "N83MZ",
|
call: "N83MZ",
|
||||||
ssid: "",
|
ssid: "",
|
||||||
isRepeated: false,
|
isRepeated: false
|
||||||
});
|
});
|
||||||
expect(result.destination).toEqual({
|
expect(result.destination).toEqual({
|
||||||
call: "T2TQ5U",
|
call: "T2TQ5U",
|
||||||
ssid: "",
|
ssid: "",
|
||||||
isRepeated: false,
|
isRepeated: false
|
||||||
});
|
});
|
||||||
expect(result.path).toHaveLength(1);
|
expect(result.path).toHaveLength(1);
|
||||||
expect(result.path[0]).toEqual({
|
expect(result.path[0]).toEqual({
|
||||||
call: "WA1PLE",
|
call: "WA1PLE",
|
||||||
ssid: "4",
|
ssid: "4",
|
||||||
isRepeated: true,
|
isRepeated: true
|
||||||
});
|
});
|
||||||
expect(result.payload).toBe("`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3");
|
expect(result.payload).toBe("`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses frame with multiple path elements", () => {
|
it("parses frame with multiple path elements", () => {
|
||||||
const data =
|
const data = "KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test";
|
||||||
"KB1ABC-5>APRS,WIDE1-1,WIDE2-2*,IGATE:!4903.50N/07201.75W-Test";
|
|
||||||
const result = Frame.fromString(data);
|
const result = Frame.fromString(data);
|
||||||
expect(result.source.call).toBe("KB1ABC");
|
expect(result.source.call).toBe("KB1ABC");
|
||||||
expect(result.path).toHaveLength(3);
|
expect(result.path).toHaveLength(3);
|
||||||
@@ -174,16 +190,12 @@ describe("Frame.fromString", () => {
|
|||||||
|
|
||||||
it("throws for frame without route separator", () => {
|
it("throws for frame without route separator", () => {
|
||||||
const data = "NOCALL-1>APRS";
|
const data = "NOCALL-1>APRS";
|
||||||
expect(() => Frame.fromString(data)).toThrow(
|
expect(() => Frame.fromString(data)).toThrow("APRS: invalid frame, no route separator found");
|
||||||
"APRS: invalid frame, no route separator found",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws for frame with invalid addresses", () => {
|
it("throws for frame with invalid addresses", () => {
|
||||||
const data = "NOCALL:payload";
|
const data = "NOCALL:payload";
|
||||||
expect(() => Frame.fromString(data)).toThrow(
|
expect(() => Frame.fromString(data)).toThrow("APRS: invalid addresses in route");
|
||||||
"APRS: invalid addresses in route",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,7 +233,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as MicEPayload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe(DataType.MicECurrent);
|
expect(decoded?.type).toBe(DataType.MicE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decodes a Mic-E packet with old format (single quote)", () => {
|
it("decodes a Mic-E packet with old format (single quote)", () => {
|
||||||
@@ -257,9 +269,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
const textSection = res.structure.find((s) => s.name === "text");
|
const textSection = res.structure.find((s) => s.name === "text");
|
||||||
expect(recipientSection).toBeDefined();
|
expect(recipientSection).toBeDefined();
|
||||||
expect(textSection).toBeDefined();
|
expect(textSection).toBeDefined();
|
||||||
expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe(
|
expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe("KB1ABC");
|
||||||
"KB1ABC",
|
|
||||||
);
|
|
||||||
expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
|
expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -291,8 +301,7 @@ describe("Frame.decodeObject", () => {
|
|||||||
|
|
||||||
describe("Frame.decodePosition", () => {
|
describe("Frame.decodePosition", () => {
|
||||||
it("decodes position with timestamp and compressed format", () => {
|
it("decodes position with timestamp and compressed format", () => {
|
||||||
const data =
|
const data = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as PositionPayload;
|
const decoded = frame.decode() as PositionPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
@@ -313,6 +322,16 @@ describe("Frame.decodePosition", () => {
|
|||||||
const decoded = frame.decode() as PositionPayload;
|
const decoded = frame.decode() as PositionPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle UTF-8 characters", () => {
|
||||||
|
const data =
|
||||||
|
"WB2OSZ-5>APDW17:!4237.14NS07120.83W#PHG7140 Did you know that APRS comments and messages can contain UTF-8 characters? アマチュア無線";
|
||||||
|
const frame = Frame.fromString(data);
|
||||||
|
const decoded = frame.decode() as PositionPayload;
|
||||||
|
expect(decoded).not.toBeNull();
|
||||||
|
expect(decoded?.type).toBe(DataType.PositionNoTimestampNoMessaging);
|
||||||
|
expect(decoded?.position.comment).toContain("UTF-8 characters? アマチュア無線");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Frame.decodeStatus", () => {
|
describe("Frame.decodeStatus", () => {
|
||||||
@@ -324,8 +343,7 @@ describe("Frame.decodeStatus", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
if (res.payload?.type !== DataType.Status)
|
if (res.payload?.type !== DataType.Status) throw new Error("expected status payload");
|
||||||
throw new Error("expected status payload");
|
|
||||||
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
|
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
|
||||||
expect(payload.text).toBe("Testing status");
|
expect(payload.text).toBe("Testing status");
|
||||||
expect(payload.maidenhead).toBe("FN20");
|
expect(payload.maidenhead).toBe("FN20");
|
||||||
@@ -423,7 +441,7 @@ describe("Timestamp.toDate", () => {
|
|||||||
if (futureHours < 24) {
|
if (futureHours < 24) {
|
||||||
const ts = new Timestamp(futureHours, 0, "HMS", {
|
const ts = new Timestamp(futureHours, 0, "HMS", {
|
||||||
seconds: 0,
|
seconds: 0,
|
||||||
zulu: true,
|
zulu: true
|
||||||
});
|
});
|
||||||
const date = ts.toDate();
|
const date = ts.toDate();
|
||||||
|
|
||||||
@@ -440,7 +458,7 @@ describe("Timestamp.toDate", () => {
|
|||||||
const ts = new Timestamp(12, 0, "MDHM", {
|
const ts = new Timestamp(12, 0, "MDHM", {
|
||||||
month: futureMonth + 1,
|
month: futureMonth + 1,
|
||||||
day: 1,
|
day: 1,
|
||||||
zulu: false,
|
zulu: false
|
||||||
});
|
});
|
||||||
const date = ts.toDate();
|
const date = ts.toDate();
|
||||||
|
|
||||||
@@ -455,15 +473,14 @@ describe("Timestamp.toDate", () => {
|
|||||||
describe("Frame.decodeMicE", () => {
|
describe("Frame.decodeMicE", () => {
|
||||||
describe("Basic Mic-E frames", () => {
|
describe("Basic Mic-E frames", () => {
|
||||||
it("should decode a basic Mic-E packet (current format)", () => {
|
it("should decode a basic Mic-E packet (current format)", () => {
|
||||||
const data =
|
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
||||||
"N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as MicEPayload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe(DataType.MicECurrent);
|
expect(decoded?.type).toBe(DataType.MicE);
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position).toBeDefined();
|
expect(decoded.position).toBeDefined();
|
||||||
expect(typeof decoded.position.latitude).toBe("number");
|
expect(typeof decoded.position.latitude).toBe("number");
|
||||||
expect(typeof decoded.position.longitude).toBe("number");
|
expect(typeof decoded.position.longitude).toBe("number");
|
||||||
@@ -490,7 +507,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3);
|
expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -502,7 +519,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3);
|
expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -514,7 +531,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
|
expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -526,7 +543,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.latitude).toBeLessThan(0);
|
expect(decoded.position.latitude).toBeLessThan(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -540,7 +557,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(typeof decoded.position.longitude).toBe("number");
|
expect(typeof decoded.position.longitude).toBe("number");
|
||||||
expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180);
|
expect(decoded.position.longitude).toBeGreaterThanOrEqual(-180);
|
||||||
expect(decoded.position.longitude).toBeLessThanOrEqual(180);
|
expect(decoded.position.longitude).toBeLessThanOrEqual(180);
|
||||||
@@ -554,7 +571,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(typeof decoded.position.longitude).toBe("number");
|
expect(typeof decoded.position.longitude).toBe("number");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -566,7 +583,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(typeof decoded.position.longitude).toBe("number");
|
expect(typeof decoded.position.longitude).toBe("number");
|
||||||
expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90);
|
expect(Math.abs(decoded.position.longitude)).toBeGreaterThan(90);
|
||||||
}
|
}
|
||||||
@@ -581,7 +598,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
if (decoded.position.speed !== undefined) {
|
if (decoded.position.speed !== undefined) {
|
||||||
expect(decoded.position.speed).toBeGreaterThanOrEqual(0);
|
expect(decoded.position.speed).toBeGreaterThanOrEqual(0);
|
||||||
expect(typeof decoded.position.speed).toBe("number");
|
expect(typeof decoded.position.speed).toBe("number");
|
||||||
@@ -596,7 +613,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
if (decoded.position.course !== undefined) {
|
if (decoded.position.course !== undefined) {
|
||||||
expect(decoded.position.course).toBeGreaterThanOrEqual(0);
|
expect(decoded.position.course).toBeGreaterThanOrEqual(0);
|
||||||
expect(decoded.position.course).toBeLessThan(360);
|
expect(decoded.position.course).toBeLessThan(360);
|
||||||
@@ -611,7 +628,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.speed).toBeUndefined();
|
expect(decoded.position.speed).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -623,7 +640,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
if (decoded.position.course !== undefined) {
|
if (decoded.position.course !== undefined) {
|
||||||
expect(decoded.position.course).toBeGreaterThan(0);
|
expect(decoded.position.course).toBeGreaterThan(0);
|
||||||
expect(decoded.position.course).toBeLessThan(360);
|
expect(decoded.position.course).toBeLessThan(360);
|
||||||
@@ -640,7 +657,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.symbol).toBeDefined();
|
expect(decoded.position.symbol).toBeDefined();
|
||||||
expect(decoded.position.symbol?.table).toBeDefined();
|
expect(decoded.position.symbol?.table).toBeDefined();
|
||||||
expect(decoded.position.symbol?.code).toBeDefined();
|
expect(decoded.position.symbol?.code).toBeDefined();
|
||||||
@@ -652,29 +669,30 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
describe("Altitude decoding", () => {
|
describe("Altitude decoding", () => {
|
||||||
it("should decode altitude from /A=NNNNNN format", () => {
|
it("should decode altitude from /A=NNNNNN format", () => {
|
||||||
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}/A=001234";
|
const data = "CALL>4ABCDE:`c.l+@&'//A=001234";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
expect(decoded.type).toBe(DataType.MicE);
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
expect(decoded.position).toBeDefined();
|
||||||
|
expect(decoded.position.altitude).toBeDefined();
|
||||||
expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
|
expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should decode altitude from base-91 format }abc", () => {
|
it("should decode altitude from base-91 format abc}", () => {
|
||||||
const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}}S^X";
|
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/\"4T}KJ6TMS";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
expect(decoded.type).toBe(DataType.MicE);
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
expect(decoded.position).toBeDefined();
|
||||||
if (decoded.position.comment?.startsWith("}")) {
|
|
||||||
expect(decoded.position.altitude).toBeDefined();
|
expect(decoded.position.altitude).toBeDefined();
|
||||||
}
|
expect(decoded.position.comment).toBe("KJ6TMS");
|
||||||
}
|
expect(decoded.position.altitude).toBeCloseTo(61, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should prefer /A= format over base-91 when both present", () => {
|
it("should prefer /A= format over base-91 when both present", () => {
|
||||||
@@ -684,7 +702,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
|
expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -696,7 +714,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.altitude).toBeUndefined();
|
expect(decoded.position.altitude).toBeUndefined();
|
||||||
expect(decoded.position.comment).toContain("Just a comment");
|
expect(decoded.position.comment).toContain("Just a comment");
|
||||||
}
|
}
|
||||||
@@ -711,7 +729,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.messageType).toBe("M0: Off Duty");
|
expect(decoded.messageType).toBe("M0: Off Duty");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -723,7 +741,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.messageType).toBeDefined();
|
expect(decoded.messageType).toBeDefined();
|
||||||
expect(typeof decoded.messageType).toBe("string");
|
expect(typeof decoded.messageType).toBe("string");
|
||||||
}
|
}
|
||||||
@@ -746,7 +764,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.comment).toContain("This is a test comment");
|
expect(decoded.position.comment).toContain("This is a test comment");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -758,7 +776,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.comment).toBeDefined();
|
expect(decoded.position.comment).toBeDefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -795,23 +813,20 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(() => frame.decode()).not.toThrow();
|
expect(() => frame.decode()).not.toThrow();
|
||||||
const decoded = frame.decode() as MicEPayload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
expect(decoded === null || decoded?.type === DataType.MicECurrent).toBe(
|
expect(decoded === null || decoded?.type === DataType.MicE).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Real-world test vectors", () => {
|
describe("Real-world test vectors", () => {
|
||||||
it("should decode real Mic-E packet from test vector 2", () => {
|
it("should decode real Mic-E packet from test vector 2", () => {
|
||||||
const data =
|
const data = "N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
||||||
"N83MZ>T2TQ5U,WA1PLE-4*:`c.l+@&'/'\"G:} KJ6TMS|!:&0'p|!w#f!|3";
|
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as MicEPayload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe(DataType.MicECurrent);
|
expect(decoded?.type).toBe(DataType.MicE);
|
||||||
|
|
||||||
if (decoded && decoded.type === DataType.MicECurrent) {
|
if (decoded && decoded.type === DataType.MicE) {
|
||||||
expect(decoded.position.latitude).toBeDefined();
|
expect(decoded.position.latitude).toBeDefined();
|
||||||
expect(decoded.position.longitude).toBeDefined();
|
expect(decoded.position.longitude).toBeDefined();
|
||||||
expect(decoded.position.symbol).toBeDefined();
|
expect(decoded.position.symbol).toBeDefined();
|
||||||
@@ -837,20 +852,16 @@ describe("Packet dissection with sections", () => {
|
|||||||
expect(result.structure).toBeDefined();
|
expect(result.structure).toBeDefined();
|
||||||
expect(result.structure.length).toBeGreaterThan(0);
|
expect(result.structure.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const routingSection = result.structure.find((s) => s.name === "Routing");
|
const routingSection = result.structure.find((s) => s.name === "routing");
|
||||||
expect(routingSection).toBeDefined();
|
expect(routingSection).toBeDefined();
|
||||||
expect(routingSection?.fields).toBeDefined();
|
expect(routingSection?.fields).toBeDefined();
|
||||||
expect(routingSection?.fields?.length).toBeGreaterThan(0);
|
expect(routingSection?.fields?.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const sourceField = routingSection?.fields?.find(
|
const sourceField = routingSection?.fields?.find((a) => a.name === "source address");
|
||||||
(a) => a.name === "Source address",
|
|
||||||
);
|
|
||||||
expect(sourceField).toBeDefined();
|
expect(sourceField).toBeDefined();
|
||||||
expect(sourceField?.length).toBeGreaterThan(0);
|
expect(sourceField?.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const destField = routingSection?.fields?.find(
|
const destField = routingSection?.fields?.find((a) => a.name === "destination address");
|
||||||
(a) => a.name === "Destination address",
|
|
||||||
);
|
|
||||||
expect(destField).toBeDefined();
|
expect(destField).toBeDefined();
|
||||||
expect(destField?.length).toBeGreaterThan(0);
|
expect(destField?.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -869,9 +880,7 @@ describe("Packet dissection with sections", () => {
|
|||||||
expect(result.structure).toBeDefined();
|
expect(result.structure).toBeDefined();
|
||||||
expect(result.structure?.length).toBeGreaterThan(0);
|
expect(result.structure?.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const positionSection = result.structure?.find(
|
const positionSection = result.structure?.find((s) => s.name === "position");
|
||||||
(s) => s.name === "position",
|
|
||||||
);
|
|
||||||
expect(positionSection).toBeDefined();
|
expect(positionSection).toBeDefined();
|
||||||
expect(positionSection?.data?.byteLength).toBe(19);
|
expect(positionSection?.data?.byteLength).toBe(19);
|
||||||
expect(positionSection?.fields).toBeDefined();
|
expect(positionSection?.fields).toBeDefined();
|
||||||
@@ -897,20 +906,16 @@ describe("Packet dissection with sections", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload?.type).toBe(
|
expect(result.payload?.type).toBe(DataType.PositionWithTimestampWithMessaging);
|
||||||
DataType.PositionWithTimestampWithMessaging,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timestampSection = result.structure?.find(
|
const timestampSection = result.structure?.find((s) => s.name === "timestamp");
|
||||||
(s) => s.name === "timestamp",
|
|
||||||
);
|
|
||||||
expect(timestampSection).toBeDefined();
|
expect(timestampSection).toBeDefined();
|
||||||
expect(timestampSection?.data?.byteLength).toBe(7);
|
expect(timestampSection?.data?.byteLength).toBe(7);
|
||||||
expect(timestampSection?.fields?.map((a) => a.name)).toEqual([
|
expect(timestampSection?.fields?.map((a) => a.name)).toEqual([
|
||||||
"day (DD)",
|
"day (DD)",
|
||||||
"hour (HH)",
|
"hour (HH)",
|
||||||
"minute (MM)",
|
"minute (MM)",
|
||||||
"timezone indicator",
|
"timezone indicator"
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -922,13 +927,9 @@ describe("Packet dissection with sections", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload?.type).toBe(
|
expect(result.payload?.type).toBe(DataType.PositionWithTimestampWithMessaging);
|
||||||
DataType.PositionWithTimestampWithMessaging,
|
|
||||||
);
|
|
||||||
|
|
||||||
const positionSection = result.structure?.find(
|
const positionSection = result.structure?.find((s) => s.name === "position");
|
||||||
(s) => s.name === "position",
|
|
||||||
);
|
|
||||||
expect(positionSection).toBeDefined();
|
expect(positionSection).toBeDefined();
|
||||||
expect(positionSection?.data?.byteLength).toBe(13);
|
expect(positionSection?.data?.byteLength).toBe(13);
|
||||||
|
|
||||||
@@ -979,9 +980,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
const textSection = res.structure.find((s) => s.name === "text");
|
const textSection = res.structure.find((s) => s.name === "text");
|
||||||
expect(recipientSection).toBeDefined();
|
expect(recipientSection).toBeDefined();
|
||||||
expect(textSection).toBeDefined();
|
expect(textSection).toBeDefined();
|
||||||
expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe(
|
expect(new TextDecoder().decode(recipientSection!.data).trim()).toBe("KB1ABC");
|
||||||
"KB1ABC",
|
|
||||||
);
|
|
||||||
expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
|
expect(new TextDecoder().decode(textSection!.data)).toBe("Test via spec");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -998,8 +997,7 @@ describe("Frame.decoding: object and status", () => {
|
|||||||
expect(res).toHaveProperty("payload");
|
expect(res).toHaveProperty("payload");
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
|
|
||||||
if (res.payload?.type !== DataType.Object)
|
if (res.payload?.type !== DataType.Object) throw new Error("expected object payload");
|
||||||
throw new Error("expected object payload");
|
|
||||||
|
|
||||||
const payload = res.payload as ObjectPayload & { timestamp?: ITimestamp };
|
const payload = res.payload as ObjectPayload & { timestamp?: ITimestamp };
|
||||||
|
|
||||||
@@ -1029,8 +1027,7 @@ describe("Frame.decoding: object and status", () => {
|
|||||||
expect(res).toHaveProperty("payload");
|
expect(res).toHaveProperty("payload");
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
|
|
||||||
if (res.payload?.type !== DataType.Status)
|
if (res.payload?.type !== DataType.Status) throw new Error("expected status payload");
|
||||||
throw new Error("expected status payload");
|
|
||||||
|
|
||||||
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
|
const payload = res.payload as StatusPayload & { timestamp?: ITimestamp };
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import type { UserDefinedPayload } from "../src/frame.types";
|
import { DataType, type UserDefinedPayload } from "../src/frame.types";
|
||||||
|
|
||||||
describe("Frame.decodeUserDefined", () => {
|
describe("Frame.decodeUserDefined", () => {
|
||||||
it("parses packet type only", () => {
|
it("parses packet type only", () => {
|
||||||
@@ -9,7 +10,7 @@ describe("Frame.decodeUserDefined", () => {
|
|||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as UserDefinedPayload;
|
const decoded = frame.decode() as UserDefinedPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded.type).toBe("user-defined");
|
expect(decoded.type).toBe(DataType.UserDefined);
|
||||||
expect(decoded.userPacketType).toBe("01");
|
expect(decoded.userPacketType).toBe("01");
|
||||||
expect(decoded.data).toBe("");
|
expect(decoded.data).toBe("");
|
||||||
});
|
});
|
||||||
@@ -22,14 +23,12 @@ describe("Frame.decodeUserDefined", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
expect(res.payload.type).toBe("user-defined");
|
expect(res.payload.type).toBe(DataType.UserDefined);
|
||||||
expect(res.payload.userPacketType).toBe("TEX");
|
expect(res.payload.userPacketType).toBe("TEX");
|
||||||
expect(res.payload.data).toBe("Hello world");
|
expect(res.payload.data).toBe("Hello world");
|
||||||
|
|
||||||
const raw = res.structure.find((s) => s.name === "user-defined");
|
const raw = res.structure.find((s) => s.name === "user-defined");
|
||||||
const typeSection = res.structure.find(
|
const typeSection = res.structure.find((s) => s.name === "user-packet-type");
|
||||||
(s) => s.name === "user-packet-type",
|
|
||||||
);
|
|
||||||
const dataSection = res.structure.find((s) => s.name === "user-data");
|
const dataSection = res.structure.find((s) => s.name === "user-data");
|
||||||
expect(raw).toBeDefined();
|
expect(raw).toBeDefined();
|
||||||
expect(typeSection).toBeDefined();
|
expect(typeSection).toBeDefined();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import { DataType, WeatherPayload } from "../src/frame.types";
|
import { DataType, WeatherPayload } from "../src/frame.types";
|
||||||
import { Dissected } from "@hamradio/packet";
|
|
||||||
|
|
||||||
describe("Frame decode - Weather", () => {
|
describe("Frame decode - Weather", () => {
|
||||||
it("parses weather with timestamp, wind, temp, rain, humidity and pressure", () => {
|
it("parses weather with timestamp, wind, temp, rain, humidity and pressure", () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
base91ToNumber,
|
base91ToNumber,
|
||||||
knotsToKmh,
|
|
||||||
kmhToKnots,
|
|
||||||
feetToMeters,
|
|
||||||
metersToFeet,
|
|
||||||
celsiusToFahrenheit,
|
celsiusToFahrenheit,
|
||||||
fahrenheitToCelsius,
|
fahrenheitToCelsius,
|
||||||
|
feetToMeters,
|
||||||
|
kmhToKnots,
|
||||||
|
knotsToKmh,
|
||||||
|
metersToFeet
|
||||||
} from "../src/parser";
|
} from "../src/parser";
|
||||||
|
|
||||||
describe("parser utilities", () => {
|
describe("parser utilities", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user