Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0ab62dab02
|
|||
|
38b617728c
|
|||
|
16f638301b
|
|||
|
d0a100359d
|
|||
|
c300aefc0b
|
|||
|
074806528f
|
118
README.md
118
README.md
@@ -0,0 +1,118 @@
|
|||||||
|
# @hamradio/aprs
|
||||||
|
|
||||||
|
APRS (Automatic Packet Reporting System) utilities and parsers for TypeScript/JavaScript.
|
||||||
|
|
||||||
|
> For AX.25 frame parsing, see [@hamradio/ax25](https://www.npmjs.com/package/@hamradio/ax25).
|
||||||
|
|
||||||
|
This package provides lightweight parsing and helpers for APRS frames (APRS-IS style payloads). It exposes a small API for parsing frames, decoding payloads, working with APRS timestamps and addresses, and a few utility conversions.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @hamradio/aprs
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with yarn:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @hamradio/aprs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick examples
|
||||||
|
|
||||||
|
Examples below show ESM / TypeScript usage. For CommonJS require() the same symbols are available from the package entrypoint.
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
Frame,
|
||||||
|
Address,
|
||||||
|
Timestamp,
|
||||||
|
base91ToNumber,
|
||||||
|
knotsToKmh,
|
||||||
|
} from '@hamradio/aprs';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parse a raw APRS frame and decode payload
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const raw = 'NOCALL-1>APRS,WIDE1-1:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
|
|
||||||
|
// Parse into a Frame instance
|
||||||
|
const frame = Frame.fromString(raw);
|
||||||
|
|
||||||
|
// Inspect routing and payload
|
||||||
|
console.log(frame.source.toString()); // e.g. NOCALL-1
|
||||||
|
console.log(frame.destination.toString()); // APRS
|
||||||
|
console.log(frame.path.map(p => p.toString()));
|
||||||
|
|
||||||
|
// Decode payload (returns a structured payload object or null)
|
||||||
|
const payload = frame.decode();
|
||||||
|
console.log(payload?.type); // e.g. 'position' | 'message' | 'status' | ...
|
||||||
|
|
||||||
|
// Or ask for sections (dissection) along with decoded payload
|
||||||
|
const res = frame.decode(true) as { payload: any | null; structure: any };
|
||||||
|
console.log(res.payload, res.structure);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message decoding
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const msg = 'W1AW>APRS::KB1ABC-5 :Hello World';
|
||||||
|
const f = Frame.fromString(msg);
|
||||||
|
const decoded = f.decode();
|
||||||
|
if (decoded && decoded.type === 'message') {
|
||||||
|
console.log(decoded.addressee); // KB1ABC-5
|
||||||
|
console.log(decoded.text); // Hello World
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Work with addresses and timestamps
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const a = Address.parse('WA1PLE-4*');
|
||||||
|
console.log(a.call, a.ssid, a.isRepeated);
|
||||||
|
|
||||||
|
const ts = new Timestamp(12, 45, 'HMS', { seconds: 30, zulu: true });
|
||||||
|
console.log(ts.toDate()); // JavaScript Date representing the timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility conversions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log(base91ToNumber('!!!!')); // decode base91 values used in some APRS payloads
|
||||||
|
console.log(knotsToKmh(10)); // convert speed
|
||||||
|
```
|
||||||
|
|
||||||
|
## API summary
|
||||||
|
|
||||||
|
- `Frame` — parse frames with `Frame.fromString()` / `Frame.parse()` and decode payloads with `frame.decode()`.
|
||||||
|
- `Address` — helpers to parse and format APRS addresses: `Address.parse()` / `Address.fromString()`.
|
||||||
|
- `Timestamp` — APRS timestamp wrapper with `toDate()` conversion.
|
||||||
|
- Utility functions: `base91ToNumber`, `knotsToKmh`, `kmhToKnots`, `feetToMeters`, `metersToFeet`, `celsiusToFahrenheit`, `fahrenheitToCelsius`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the distribution with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See the project repository for contribution guidelines and tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Project: @hamradio/aprs — APRS parsing utilities for TypeScript
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hamradio/aprs",
|
"name": "@hamradio/aprs",
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"description": "APRS (Automatic Packet Reporting System) protocol support for Typescript",
|
"description": "APRS (Automatic Packet Reporting System) protocol support for Typescript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"APRS",
|
"APRS",
|
||||||
|
|||||||
168
src/frame.ts
168
src/frame.ts
@@ -1,26 +1,28 @@
|
|||||||
import type { Dissected, Segment, Field } from "@hamradio/packet";
|
import type { Dissected, Segment, Field } from "@hamradio/packet";
|
||||||
import { FieldType } from "@hamradio/packet";
|
import { FieldType } from "@hamradio/packet";
|
||||||
import type {
|
import {
|
||||||
IAddress,
|
type IAddress,
|
||||||
IFrame,
|
type IFrame,
|
||||||
Payload,
|
type Payload,
|
||||||
ITimestamp,
|
type ITimestamp,
|
||||||
PositionPayload,
|
type PositionPayload,
|
||||||
MessagePayload,
|
type MessagePayload,
|
||||||
IPosition,
|
type IPosition,
|
||||||
ObjectPayload,
|
type ObjectPayload,
|
||||||
ItemPayload,
|
type ItemPayload,
|
||||||
StatusPayload,
|
type StatusPayload,
|
||||||
QueryPayload,
|
type QueryPayload,
|
||||||
TelemetryDataPayload,
|
type TelemetryDataPayload,
|
||||||
TelemetryBitSensePayload,
|
type TelemetryBitSensePayload,
|
||||||
TelemetryCoefficientsPayload,
|
type TelemetryCoefficientsPayload,
|
||||||
TelemetryParameterPayload,
|
type TelemetryParameterPayload,
|
||||||
TelemetryUnitPayload,
|
type TelemetryUnitPayload,
|
||||||
WeatherPayload,
|
type WeatherPayload,
|
||||||
RawGPSPayload,
|
type RawGPSPayload,
|
||||||
StationCapabilitiesPayload,
|
type StationCapabilitiesPayload,
|
||||||
ThirdPartyPayload,
|
type ThirdPartyPayload,
|
||||||
|
DataType,
|
||||||
|
MicEPayload,
|
||||||
} from "./frame.types";
|
} from "./frame.types";
|
||||||
import { Position } from "./position";
|
import { Position } from "./position";
|
||||||
import { base91ToNumber } from "./parser";
|
import { base91ToNumber } from "./parser";
|
||||||
@@ -376,6 +378,13 @@ export class Frame implements IFrame {
|
|||||||
if (routingSection) {
|
if (routingSection) {
|
||||||
structure.push(routingSection);
|
structure.push(routingSection);
|
||||||
}
|
}
|
||||||
|
// Add data type identifier section
|
||||||
|
structure.push({
|
||||||
|
name: "data type",
|
||||||
|
data: new TextEncoder().encode(this.payload.charAt(0)).buffer,
|
||||||
|
isString: true,
|
||||||
|
fields: [{ type: FieldType.CHAR, name: "identifier", length: 1 }],
|
||||||
|
});
|
||||||
if (payloadsegment) {
|
if (payloadsegment) {
|
||||||
structure.push(...payloadsegment);
|
structure.push(...payloadsegment);
|
||||||
}
|
}
|
||||||
@@ -498,8 +507,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 = {
|
const payload: PositionPayload = {
|
||||||
type: "position",
|
type: payloadType,
|
||||||
timestamp,
|
timestamp,
|
||||||
position,
|
position,
|
||||||
messaging,
|
messaging,
|
||||||
@@ -897,8 +928,20 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: PositionPayload = {
|
let payloadType: DataType.MicECurrent | DataType.MicEOld;
|
||||||
type: "position",
|
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: {
|
position: {
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
@@ -907,11 +950,8 @@ export class Frame implements IFrame {
|
|||||||
code: symbolCode,
|
code: symbolCode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messaging: true, // Mic-E is always messaging-capable
|
messageType,
|
||||||
micE: {
|
isStandard,
|
||||||
messageType,
|
|
||||||
isStandard,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (speed > 0) {
|
if (speed > 0) {
|
||||||
@@ -1133,6 +1173,13 @@ export class Frame implements IFrame {
|
|||||||
text = this.payload.substring(textStart + 1);
|
text = this.payload.substring(textStart + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payload: MessagePayload = {
|
||||||
|
type: DataType.Message,
|
||||||
|
variant: "message",
|
||||||
|
addressee: recipient,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
if (withStructure) {
|
if (withStructure) {
|
||||||
// Emit text section
|
// Emit text section
|
||||||
segments.push({
|
segments.push({
|
||||||
@@ -1142,21 +1189,9 @@ export class Frame implements IFrame {
|
|||||||
fields: [{ type: FieldType.STRING, name: "text", length: text.length }],
|
fields: [{ type: FieldType.STRING, name: "text", length: text.length }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload: MessagePayload = {
|
|
||||||
type: "message",
|
|
||||||
addressee: recipient,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { payload, segment: segments };
|
return { payload, segment: segments };
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: MessagePayload = {
|
|
||||||
type: "message",
|
|
||||||
addressee: recipient,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { payload };
|
return { payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,7 +1320,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: ObjectPayload = {
|
const payload: ObjectPayload = {
|
||||||
type: "object",
|
type: DataType.Object,
|
||||||
name,
|
name,
|
||||||
timestamp,
|
timestamp,
|
||||||
alive,
|
alive,
|
||||||
@@ -1420,7 +1455,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: ItemPayload = {
|
const payload: ItemPayload = {
|
||||||
type: "item",
|
type: DataType.Item,
|
||||||
name,
|
name,
|
||||||
alive,
|
alive,
|
||||||
position,
|
position,
|
||||||
@@ -1472,7 +1507,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: StatusPayload = {
|
const payload: StatusPayload = {
|
||||||
type: "status",
|
type: DataType.Status,
|
||||||
timestamp: undefined,
|
timestamp: undefined,
|
||||||
text: statusText,
|
text: statusText,
|
||||||
};
|
};
|
||||||
@@ -1557,7 +1592,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: QueryPayload = {
|
const payload: QueryPayload = {
|
||||||
type: "query",
|
type: DataType.Query,
|
||||||
queryType,
|
queryType,
|
||||||
...(target ? { target } : {}),
|
...(target ? { target } : {}),
|
||||||
};
|
};
|
||||||
@@ -1639,7 +1674,8 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: TelemetryDataPayload = {
|
const payload: TelemetryDataPayload = {
|
||||||
type: "telemetry-data",
|
type: DataType.TelemetryData,
|
||||||
|
variant: "data",
|
||||||
sequence: isNaN(seq) ? 0 : seq,
|
sequence: isNaN(seq) ? 0 : seq,
|
||||||
analog,
|
analog,
|
||||||
digital: isNaN(digital) ? 0 : digital,
|
digital: isNaN(digital) ? 0 : digital,
|
||||||
@@ -1664,7 +1700,8 @@ export class Frame implements IFrame {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const payload: TelemetryParameterPayload = {
|
const payload: TelemetryParameterPayload = {
|
||||||
type: "telemetry-parameters",
|
type: DataType.TelemetryData,
|
||||||
|
variant: "parameters",
|
||||||
names,
|
names,
|
||||||
};
|
};
|
||||||
if (withStructure) return { payload, segment: segments };
|
if (withStructure) return { payload, segment: segments };
|
||||||
@@ -1686,7 +1723,8 @@ export class Frame implements IFrame {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const payload: TelemetryUnitPayload = {
|
const payload: TelemetryUnitPayload = {
|
||||||
type: "telemetry-units",
|
type: DataType.TelemetryData,
|
||||||
|
variant: "unit",
|
||||||
units,
|
units,
|
||||||
};
|
};
|
||||||
if (withStructure) return { payload, segment: segments };
|
if (withStructure) return { payload, segment: segments };
|
||||||
@@ -1717,7 +1755,8 @@ export class Frame implements IFrame {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const payload: TelemetryCoefficientsPayload = {
|
const payload: TelemetryCoefficientsPayload = {
|
||||||
type: "telemetry-coefficients",
|
type: DataType.TelemetryData,
|
||||||
|
variant: "coefficients",
|
||||||
coefficients,
|
coefficients,
|
||||||
};
|
};
|
||||||
if (withStructure) return { payload, segment: segments };
|
if (withStructure) return { payload, segment: segments };
|
||||||
@@ -1741,7 +1780,8 @@ export class Frame implements IFrame {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const payload: TelemetryBitSensePayload = {
|
const payload: TelemetryBitSensePayload = {
|
||||||
type: "telemetry-bitsense",
|
type: DataType.TelemetryData,
|
||||||
|
variant: "bitsense",
|
||||||
sense: isNaN(sense) ? 0 : sense,
|
sense: isNaN(sense) ? 0 : sense,
|
||||||
...(projectName ? { projectName } : {}),
|
...(projectName ? { projectName } : {}),
|
||||||
};
|
};
|
||||||
@@ -1818,7 +1858,9 @@ export class Frame implements IFrame {
|
|||||||
|
|
||||||
const rest = this.payload.substring(offset).trim();
|
const rest = this.payload.substring(offset).trim();
|
||||||
|
|
||||||
const payload: WeatherPayload = { type: "weather" };
|
const payload: WeatherPayload = {
|
||||||
|
type: DataType.WeatherReportNoPosition,
|
||||||
|
};
|
||||||
if (timestamp) payload.timestamp = timestamp;
|
if (timestamp) payload.timestamp = timestamp;
|
||||||
if (position) payload.position = position;
|
if (position) payload.position = position;
|
||||||
|
|
||||||
@@ -1894,7 +1936,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: RawGPSPayload = {
|
const payload: RawGPSPayload = {
|
||||||
type: "raw-gps",
|
type: DataType.RawGPS,
|
||||||
sentence,
|
sentence,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2055,7 +2097,7 @@ export class Frame implements IFrame {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const payload: StationCapabilitiesPayload = {
|
const payload: StationCapabilitiesPayload = {
|
||||||
type: "capabilities",
|
type: DataType.StationCapabilities,
|
||||||
capabilities: tokens,
|
capabilities: tokens,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -2118,7 +2160,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payloadObj = {
|
const payloadObj = {
|
||||||
type: "user-defined",
|
type: DataType.UserDefined,
|
||||||
userPacketType,
|
userPacketType,
|
||||||
data,
|
data,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -2185,7 +2227,7 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payloadObj: ThirdPartyPayload = {
|
const payloadObj: ThirdPartyPayload = {
|
||||||
type: "third-party",
|
type: DataType.ThirdParty,
|
||||||
comment: rest,
|
comment: rest,
|
||||||
...(nestedFrame ? { frame: nestedFrame } : {}),
|
...(nestedFrame ? { frame: nestedFrame } : {}),
|
||||||
} as const;
|
} as const;
|
||||||
@@ -2271,34 +2313,34 @@ const parseFrame = (data: string): Frame => {
|
|||||||
|
|
||||||
pathFields.push({
|
pathFields.push({
|
||||||
type: FieldType.CHAR,
|
type: FieldType.CHAR,
|
||||||
name: `Path separator ${i}`,
|
name: `path separator ${i}`,
|
||||||
length: 1,
|
length: 1,
|
||||||
});
|
});
|
||||||
pathFields.push({
|
pathFields.push({
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
name: `Repeater ${i}`,
|
name: `repeater ${i}`,
|
||||||
length: pathStr.length,
|
length: pathStr.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const routingSection: Segment = {
|
const routingSection: Segment = {
|
||||||
name: "Routing",
|
name: "routing",
|
||||||
data: encoder.encode(data.slice(0, routeSepIndex)).buffer,
|
data: encoder.encode(data.slice(0, routeSepIndex + 1)).buffer,
|
||||||
isString: true,
|
isString: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
name: "Source address",
|
name: "source address",
|
||||||
length: sourceStr.length,
|
length: sourceStr.length,
|
||||||
},
|
},
|
||||||
{ type: FieldType.CHAR, name: "Route separator", length: 1 },
|
{ type: FieldType.CHAR, name: "route separator", length: 1 },
|
||||||
{
|
{
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
name: "Destination address",
|
name: "destination address",
|
||||||
length: destinationStr.length,
|
length: destinationStr.length,
|
||||||
},
|
},
|
||||||
...pathFields,
|
...pathFields,
|
||||||
{ type: FieldType.CHAR, name: "Payload separator", length: 1 },
|
{ type: FieldType.CHAR, name: "payload separator", length: 1 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,54 +14,51 @@ export interface IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// APRS Data Type Identifiers (first character of payload)
|
// APRS Data Type Identifiers (first character of payload)
|
||||||
export const DataTypeIdentifier = {
|
export enum DataType {
|
||||||
// Position Reports
|
// Position Reports
|
||||||
PositionNoTimestampNoMessaging: "!",
|
PositionNoTimestampNoMessaging = "!",
|
||||||
PositionNoTimestampWithMessaging: "=",
|
PositionNoTimestampWithMessaging = "=",
|
||||||
PositionWithTimestampNoMessaging: "/",
|
PositionWithTimestampNoMessaging = "/",
|
||||||
PositionWithTimestampWithMessaging: "@",
|
PositionWithTimestampWithMessaging = "@",
|
||||||
|
|
||||||
// Mic-E
|
// Mic-E
|
||||||
MicECurrent: "`",
|
MicECurrent = "`",
|
||||||
MicEOld: "'",
|
MicEOld = "'",
|
||||||
|
|
||||||
// Messages and Bulletins
|
// Messages and Bulletins
|
||||||
Message: ":",
|
Message = ":",
|
||||||
|
|
||||||
// Objects and Items
|
// Objects and Items
|
||||||
Object: ";",
|
Object = ";",
|
||||||
Item: ")",
|
Item = ")",
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
Status: ">",
|
Status = ">",
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
Query: "?",
|
Query = "?",
|
||||||
|
|
||||||
// Telemetry
|
// Telemetry
|
||||||
TelemetryData: "T",
|
TelemetryData = "T",
|
||||||
|
|
||||||
// Weather
|
// Weather
|
||||||
WeatherReportNoPosition: "_",
|
WeatherReportNoPosition = "_",
|
||||||
|
|
||||||
// Raw GPS Data
|
// Raw GPS Data
|
||||||
RawGPS: "$",
|
RawGPS = "$",
|
||||||
|
|
||||||
// Station Capabilities
|
// Station Capabilities
|
||||||
StationCapabilities: "<",
|
StationCapabilities = "<",
|
||||||
|
|
||||||
// User-Defined
|
// User-Defined
|
||||||
UserDefined: "{",
|
UserDefined = "{",
|
||||||
|
|
||||||
// Third-Party Traffic
|
// Third-Party Traffic
|
||||||
ThirdParty: "}",
|
ThirdParty = "}",
|
||||||
|
|
||||||
// Invalid/Test Data
|
// Invalid/Test Data
|
||||||
InvalidOrTest: ",",
|
InvalidOrTest = ",",
|
||||||
} as const;
|
}
|
||||||
|
|
||||||
export type DataTypeIdentifier =
|
|
||||||
(typeof DataTypeIdentifier)[keyof typeof DataTypeIdentifier];
|
|
||||||
|
|
||||||
export interface ISymbol {
|
export interface ISymbol {
|
||||||
table: string; // Symbol table identifier
|
table: string; // Symbol table identifier
|
||||||
@@ -99,7 +96,11 @@ export interface ITimestamp {
|
|||||||
|
|
||||||
// Position Report Payload
|
// Position Report Payload
|
||||||
export interface PositionPayload {
|
export interface PositionPayload {
|
||||||
type: "position";
|
type:
|
||||||
|
| DataType.PositionNoTimestampNoMessaging
|
||||||
|
| DataType.PositionNoTimestampWithMessaging
|
||||||
|
| DataType.PositionWithTimestampNoMessaging
|
||||||
|
| DataType.PositionWithTimestampWithMessaging;
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
messaging: boolean; // Whether APRS messaging is enabled
|
messaging: boolean; // Whether APRS messaging is enabled
|
||||||
@@ -128,19 +129,20 @@ export interface CompressedPosition {
|
|||||||
|
|
||||||
// Mic-E Payload (compressed in destination address)
|
// Mic-E Payload (compressed in destination address)
|
||||||
export interface MicEPayload {
|
export interface MicEPayload {
|
||||||
type: "mic-e";
|
type: DataType.MicECurrent | DataType.MicEOld;
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
course?: number;
|
|
||||||
speed?: number;
|
|
||||||
altitude?: number;
|
|
||||||
messageType?: string; // Standard Mic-E message
|
messageType?: string; // Standard Mic-E message
|
||||||
|
isStandard?: boolean; // Whether messageType is a standard Mic-E message
|
||||||
telemetry?: number[]; // Optional telemetry channels
|
telemetry?: number[]; // Optional telemetry channels
|
||||||
status?: string;
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MessageVariant = "message" | "bulletin";
|
||||||
|
|
||||||
// Message Payload
|
// Message Payload
|
||||||
export interface MessagePayload {
|
export interface MessagePayload {
|
||||||
type: "message";
|
type: DataType.Message;
|
||||||
|
variant: "message";
|
||||||
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
|
||||||
@@ -150,7 +152,8 @@ export interface MessagePayload {
|
|||||||
|
|
||||||
// Bulletin/Announcement (variant of message)
|
// Bulletin/Announcement (variant of message)
|
||||||
export interface BulletinPayload {
|
export interface BulletinPayload {
|
||||||
type: "bulletin";
|
type: DataType.Message;
|
||||||
|
variant: "bulletin";
|
||||||
bulletinId: string; // Bulletin identifier (BLN#)
|
bulletinId: string; // Bulletin identifier (BLN#)
|
||||||
text: string;
|
text: string;
|
||||||
group?: string; // Optional group bulletin
|
group?: string; // Optional group bulletin
|
||||||
@@ -158,7 +161,7 @@ export interface BulletinPayload {
|
|||||||
|
|
||||||
// Object Payload
|
// Object Payload
|
||||||
export interface ObjectPayload {
|
export interface ObjectPayload {
|
||||||
type: "object";
|
type: DataType.Object;
|
||||||
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
|
||||||
@@ -169,7 +172,7 @@ export interface ObjectPayload {
|
|||||||
|
|
||||||
// Item Payload
|
// Item Payload
|
||||||
export interface ItemPayload {
|
export interface ItemPayload {
|
||||||
type: "item";
|
type: DataType.Item;
|
||||||
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;
|
||||||
@@ -177,7 +180,7 @@ export interface ItemPayload {
|
|||||||
|
|
||||||
// Status Payload
|
// Status Payload
|
||||||
export interface StatusPayload {
|
export interface StatusPayload {
|
||||||
type: "status";
|
type: DataType.Status;
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
text: string;
|
text: string;
|
||||||
maidenhead?: string; // Optional Maidenhead grid locator
|
maidenhead?: string; // Optional Maidenhead grid locator
|
||||||
@@ -189,14 +192,22 @@ export interface StatusPayload {
|
|||||||
|
|
||||||
// Query Payload
|
// Query Payload
|
||||||
export interface QueryPayload {
|
export interface QueryPayload {
|
||||||
type: "query";
|
type: DataType.Query;
|
||||||
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
|
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
|
||||||
target?: string; // Target callsign or area
|
target?: string; // Target callsign or area
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TelemetryVariant =
|
||||||
|
| "data"
|
||||||
|
| "parameters"
|
||||||
|
| "unit"
|
||||||
|
| "coefficients"
|
||||||
|
| "bitsense";
|
||||||
|
|
||||||
// Telemetry Data Payload
|
// Telemetry Data Payload
|
||||||
export interface TelemetryDataPayload {
|
export interface TelemetryDataPayload {
|
||||||
type: "telemetry-data";
|
type: DataType.TelemetryData;
|
||||||
|
variant: "data";
|
||||||
sequence: number;
|
sequence: number;
|
||||||
analog: number[]; // Up to 5 analog channels
|
analog: number[]; // Up to 5 analog channels
|
||||||
digital: number; // 8-bit digital value
|
digital: number; // 8-bit digital value
|
||||||
@@ -204,19 +215,22 @@ export interface TelemetryDataPayload {
|
|||||||
|
|
||||||
// Telemetry Parameter Names
|
// Telemetry Parameter Names
|
||||||
export interface TelemetryParameterPayload {
|
export interface TelemetryParameterPayload {
|
||||||
type: "telemetry-parameters";
|
type: DataType.TelemetryData;
|
||||||
|
variant: "parameters";
|
||||||
names: string[]; // Parameter names
|
names: string[]; // Parameter names
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telemetry Unit/Label
|
// Telemetry Unit/Label
|
||||||
export interface TelemetryUnitPayload {
|
export interface TelemetryUnitPayload {
|
||||||
type: "telemetry-units";
|
type: DataType.TelemetryData;
|
||||||
|
variant: "unit";
|
||||||
units: string[]; // Units for each parameter
|
units: string[]; // Units for each parameter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telemetry Coefficients
|
// Telemetry Coefficients
|
||||||
export interface TelemetryCoefficientsPayload {
|
export interface TelemetryCoefficientsPayload {
|
||||||
type: "telemetry-coefficients";
|
type: DataType.TelemetryData;
|
||||||
|
variant: "coefficients";
|
||||||
coefficients: {
|
coefficients: {
|
||||||
a: number[]; // a coefficients
|
a: number[]; // a coefficients
|
||||||
b: number[]; // b coefficients
|
b: number[]; // b coefficients
|
||||||
@@ -226,14 +240,15 @@ export interface TelemetryCoefficientsPayload {
|
|||||||
|
|
||||||
// Telemetry Bit Sense/Project Name
|
// Telemetry Bit Sense/Project Name
|
||||||
export interface TelemetryBitSensePayload {
|
export interface TelemetryBitSensePayload {
|
||||||
type: "telemetry-bitsense";
|
type: DataType.TelemetryData;
|
||||||
|
variant: "bitsense";
|
||||||
sense: number; // 8-bit sense value
|
sense: number; // 8-bit sense value
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather Report Payload
|
// Weather Report Payload
|
||||||
export interface WeatherPayload {
|
export interface WeatherPayload {
|
||||||
type: "weather";
|
type: DataType.WeatherReportNoPosition;
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
position?: IPosition;
|
position?: IPosition;
|
||||||
windDirection?: number; // Degrees
|
windDirection?: number; // Degrees
|
||||||
@@ -255,34 +270,33 @@ export interface WeatherPayload {
|
|||||||
|
|
||||||
// Raw GPS Payload (NMEA sentences)
|
// Raw GPS Payload (NMEA sentences)
|
||||||
export interface RawGPSPayload {
|
export interface RawGPSPayload {
|
||||||
type: "raw-gps";
|
type: DataType.RawGPS;
|
||||||
sentence: string; // Raw NMEA sentence
|
sentence: string; // Raw NMEA sentence
|
||||||
position?: IPosition; // Optional parsed position if available
|
position?: IPosition; // Optional parsed position if available
|
||||||
}
|
}
|
||||||
|
|
||||||
// Station Capabilities Payload
|
// Station Capabilities Payload
|
||||||
export interface StationCapabilitiesPayload {
|
export interface StationCapabilitiesPayload {
|
||||||
type: "capabilities";
|
type: DataType.StationCapabilities;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// User-Defined Payload
|
// User-Defined Payload
|
||||||
export interface UserDefinedPayload {
|
export interface UserDefinedPayload {
|
||||||
type: "user-defined";
|
type: DataType.UserDefined;
|
||||||
userPacketType: string;
|
userPacketType: string;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third-Party Traffic Payload
|
// Third-Party Traffic Payload
|
||||||
export interface ThirdPartyPayload {
|
export interface ThirdPartyPayload {
|
||||||
type: "third-party";
|
type: DataType.ThirdParty;
|
||||||
frame?: IFrame; // Optional nested frame if payload contains another APRS frame
|
frame?: IFrame; // Optional nested frame if payload contains another APRS frame
|
||||||
comment?: string; // Optional comment
|
comment?: string; // Optional comment
|
||||||
}
|
}
|
||||||
|
|
||||||
// DF Report Payload
|
// DF Report Payload
|
||||||
export interface DFReportPayload {
|
export interface DFReportPayload {
|
||||||
type: "df-report";
|
|
||||||
timestamp?: ITimestamp;
|
timestamp?: ITimestamp;
|
||||||
position: IPosition;
|
position: IPosition;
|
||||||
course?: number;
|
course?: number;
|
||||||
@@ -295,7 +309,7 @@ export interface DFReportPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BasePayload {
|
export interface BasePayload {
|
||||||
type: string;
|
type: DataType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union type for all decoded payload types
|
// Union type for all decoded payload types
|
||||||
@@ -319,7 +333,6 @@ export type Payload = BasePayload &
|
|||||||
| StationCapabilitiesPayload
|
| StationCapabilitiesPayload
|
||||||
| UserDefinedPayload
|
| UserDefinedPayload
|
||||||
| ThirdPartyPayload
|
| ThirdPartyPayload
|
||||||
| DFReportPayload
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extended Frame with decoded payload
|
// Extended Frame with decoded payload
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
export { Frame, Address, Timestamp } from "./frame";
|
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 {
|
export {
|
||||||
|
DataType,
|
||||||
type ISymbol,
|
type ISymbol,
|
||||||
type IPosition,
|
type IPosition,
|
||||||
type ITimestamp,
|
type ITimestamp,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import type { Payload, StationCapabilitiesPayload } from "../src/frame.types";
|
import {
|
||||||
|
DataType,
|
||||||
|
type Payload,
|
||||||
|
type StationCapabilitiesPayload,
|
||||||
|
} from "../src/frame.types";
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
|
||||||
describe("Frame.decodeCapabilities", () => {
|
describe("Frame.decodeCapabilities", () => {
|
||||||
@@ -9,7 +13,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 +27,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");
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { expect } from "vitest";
|
|||||||
import { describe, it } from "vitest";
|
import { describe, it } from "vitest";
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import { QueryPayload } from "../src/frame.types";
|
import { DataType, QueryPayload } from "../src/frame.types";
|
||||||
|
|
||||||
describe("Frame decode - Query", () => {
|
describe("Frame decode - Query", () => {
|
||||||
it("decodes simple query without target", () => {
|
it("decodes simple query without target", () => {
|
||||||
const frame = Frame.fromString("SRC>DEST:?APRS");
|
const frame = Frame.fromString("SRC>DEST:?APRS");
|
||||||
const payload = frame.decode() as QueryPayload;
|
const payload = frame.decode() as QueryPayload;
|
||||||
expect(payload).not.toBeNull();
|
expect(payload).not.toBeNull();
|
||||||
expect(payload.type).toBe("query");
|
expect(payload.type).toBe(DataType.Query);
|
||||||
expect(payload.queryType).toBe("APRS");
|
expect(payload.queryType).toBe("APRS");
|
||||||
expect(payload.target).toBeUndefined();
|
expect(payload.target).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -18,7 +18,7 @@ describe("Frame decode - Query", () => {
|
|||||||
const frame = Frame.fromString("SRC>DEST:?PING N0CALL");
|
const frame = Frame.fromString("SRC>DEST:?PING N0CALL");
|
||||||
const payload = frame.decode() as QueryPayload;
|
const payload = frame.decode() as QueryPayload;
|
||||||
expect(payload).not.toBeNull();
|
expect(payload).not.toBeNull();
|
||||||
expect(payload.type).toBe("query");
|
expect(payload.type).toBe(DataType.Query);
|
||||||
expect(payload.queryType).toBe("PING");
|
expect(payload.queryType).toBe("PING");
|
||||||
expect(payload.target).toBe("N0CALL");
|
expect(payload.target).toBe("N0CALL");
|
||||||
});
|
});
|
||||||
@@ -30,7 +30,7 @@ describe("Frame decode - Query", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(result).toHaveProperty("payload");
|
expect(result).toHaveProperty("payload");
|
||||||
expect(result.payload.type).toBe("query");
|
expect(result.payload.type).toBe(DataType.Query);
|
||||||
expect(Array.isArray(result.structure)).toBe(true);
|
expect(Array.isArray(result.structure)).toBe(true);
|
||||||
const names = result.structure.map((s) => s.name);
|
const names = result.structure.map((s) => s.name);
|
||||||
expect(names).toContain("query type");
|
expect(names).toContain("query type");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import type { RawGPSPayload } from "../src/frame.types";
|
import { DataType, type RawGPSPayload } from "../src/frame.types";
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
|
||||||
describe("Raw GPS decoding", () => {
|
describe("Raw GPS decoding", () => {
|
||||||
@@ -13,7 +13,7 @@ describe("Raw GPS decoding", () => {
|
|||||||
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");
|
||||||
@@ -32,7 +32,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");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
TelemetryUnitPayload,
|
TelemetryUnitPayload,
|
||||||
TelemetryCoefficientsPayload,
|
TelemetryCoefficientsPayload,
|
||||||
TelemetryBitSensePayload,
|
TelemetryBitSensePayload,
|
||||||
|
DataType,
|
||||||
} from "../src/frame.types";
|
} from "../src/frame.types";
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import { expect } from "vitest";
|
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 frame = Frame.fromString("SRC>DEST:T#1 10,20,30,40,50 7");
|
||||||
const payload = frame.decode() as TelemetryDataPayload;
|
const payload = frame.decode() as TelemetryDataPayload;
|
||||||
expect(payload).not.toBeNull();
|
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(payload.sequence).toBe(1);
|
||||||
expect(Array.isArray(payload.analog)).toBe(true);
|
expect(Array.isArray(payload.analog)).toBe(true);
|
||||||
expect(payload.analog.length).toBe(5);
|
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 frame = Frame.fromString("SRC>DEST:TPARAM Temp,Hum,Wind");
|
||||||
const payload = frame.decode() as TelemetryParameterPayload;
|
const payload = frame.decode() as TelemetryParameterPayload;
|
||||||
expect(payload).not.toBeNull();
|
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(Array.isArray(payload.names)).toBe(true);
|
||||||
expect(payload.names).toEqual(["Temp", "Hum", "Wind"]);
|
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 frame = Frame.fromString("SRC>DEST:TUNIT C,% ,mph");
|
||||||
const payload = frame.decode() as TelemetryUnitPayload;
|
const payload = frame.decode() as TelemetryUnitPayload;
|
||||||
expect(payload).not.toBeNull();
|
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"]);
|
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 frame = Frame.fromString("SRC>DEST:TCOEFF A:1,2 B:3,4 C:5,6");
|
||||||
const payload = frame.decode() as TelemetryCoefficientsPayload;
|
const payload = frame.decode() as TelemetryCoefficientsPayload;
|
||||||
expect(payload).not.toBeNull();
|
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.a).toEqual([1, 2]);
|
||||||
expect(payload.coefficients.b).toEqual([3, 4]);
|
expect(payload.coefficients.b).toEqual([3, 4]);
|
||||||
expect(payload.coefficients.c).toEqual([5, 6]);
|
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 frame = Frame.fromString("SRC>DEST:TBITS 255 ProjectX");
|
||||||
const payload = frame.decode() as TelemetryBitSensePayload;
|
const payload = frame.decode() as TelemetryBitSensePayload;
|
||||||
expect(payload).not.toBeNull();
|
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.sense).toBe(255);
|
||||||
expect(payload.projectName).toBe("ProjectX");
|
expect(payload.projectName).toBe("ProjectX");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
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 type {
|
import {
|
||||||
Payload,
|
type Payload,
|
||||||
PositionPayload,
|
type PositionPayload,
|
||||||
ObjectPayload,
|
type ObjectPayload,
|
||||||
StatusPayload,
|
type StatusPayload,
|
||||||
ITimestamp,
|
type ITimestamp,
|
||||||
MessagePayload,
|
type MessagePayload,
|
||||||
|
DataType,
|
||||||
|
MicEPayload,
|
||||||
} from "../src/frame.types";
|
} from "../src/frame.types";
|
||||||
import { Dissected, FieldType } from "@hamradio/packet";
|
import { Dissected, FieldType } from "@hamradio/packet";
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ describe("Frame.decode (basic)", () => {
|
|||||||
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();
|
||||||
expect(decoded?.type).toBe("position");
|
expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle various data type identifiers without throwing", () => {
|
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)", () => {
|
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 data = "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 Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
expect(decoded).not.toBeNull();
|
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)", () => {
|
it("decodes a Mic-E packet with old format (single quote)", () => {
|
||||||
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
|
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
|
||||||
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("position");
|
expect(decoded?.type).toBe(DataType.MicEOld);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +239,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
const frame = Frame.fromString(raw);
|
const frame = Frame.fromString(raw);
|
||||||
const decoded = frame.decode() as MessagePayload;
|
const decoded = frame.decode() as MessagePayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe("message");
|
expect(decoded?.type).toBe(DataType.Message);
|
||||||
expect(decoded?.addressee).toBe("KB1ABC-5");
|
expect(decoded?.addressee).toBe("KB1ABC-5");
|
||||||
expect(decoded?.text).toBe("Hello World");
|
expect(decoded?.text).toBe("Hello World");
|
||||||
});
|
});
|
||||||
@@ -250,7 +252,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(res.payload).not.toBeNull();
|
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 recipientSection = res.structure.find((s) => s.name === "recipient");
|
||||||
const textSection = res.structure.find((s) => s.name === "text");
|
const textSection = res.structure.find((s) => s.name === "text");
|
||||||
expect(recipientSection).toBeDefined();
|
expect(recipientSection).toBeDefined();
|
||||||
@@ -266,10 +268,10 @@ describe("Frame.decodeObject", () => {
|
|||||||
it("decodes object payload with uncompressed position", () => {
|
it("decodes object payload with uncompressed position", () => {
|
||||||
const data = "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object";
|
const data = "CALL>APRS:;OBJECT *092345z4903.50N/07201.75W>Test object";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const decoded = frame.decode() as Payload;
|
const decoded = frame.decode() as ObjectPayload;
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe("object");
|
expect(decoded?.type).toBe(DataType.Object);
|
||||||
if (decoded && decoded.type === "object") {
|
if (decoded && decoded.type === DataType.Object) {
|
||||||
expect(decoded.name).toBe("OBJECT");
|
expect(decoded.name).toBe("OBJECT");
|
||||||
expect(decoded.alive).toBe(true);
|
expect(decoded.alive).toBe(true);
|
||||||
}
|
}
|
||||||
@@ -294,7 +296,7 @@ describe("Frame.decodePosition", () => {
|
|||||||
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();
|
||||||
expect(decoded?.type).toBe("position");
|
expect(decoded?.type).toBe(DataType.PositionWithTimestampWithMessaging);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decodes uncompressed position without timestamp", () => {
|
it("decodes uncompressed position without timestamp", () => {
|
||||||
@@ -302,7 +304,7 @@ describe("Frame.decodePosition", () => {
|
|||||||
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();
|
||||||
expect(decoded?.type).toBe("position");
|
expect(decoded?.type).toBe(DataType.PositionNoTimestampNoMessaging);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles ambiguity masking in position", () => {
|
it("handles ambiguity masking in position", () => {
|
||||||
@@ -322,7 +324,7 @@ describe("Frame.decodeStatus", () => {
|
|||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(res.payload).not.toBeNull();
|
expect(res.payload).not.toBeNull();
|
||||||
if (res.payload?.type !== "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");
|
||||||
@@ -349,7 +351,7 @@ describe("Frame.decode (sections)", () => {
|
|||||||
payload: PositionPayload | null;
|
payload: PositionPayload | null;
|
||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
expect(result.payload?.type).toBe("position");
|
expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -456,29 +458,27 @@ describe("Frame.decodeMicE", () => {
|
|||||||
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 Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe("position");
|
expect(decoded?.type).toBe(DataType.MicECurrent);
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.messaging).toBe(true);
|
|
||||||
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");
|
||||||
expect(decoded.position.symbol).toBeDefined();
|
expect(decoded.position.symbol).toBeDefined();
|
||||||
expect(decoded.micE).toBeDefined();
|
expect(decoded.messageType).toBeDefined();
|
||||||
expect(decoded.micE?.messageType).toBeDefined();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should decode a Mic-E packet with old format (single quote)", () => {
|
it("should decode a Mic-E packet with old format (single quote)", () => {
|
||||||
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
|
const data = "CALL>T2TQ5U:'c.l+@&'/'\"G:}";
|
||||||
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("position");
|
expect(decoded?.type).toBe(DataType.MicEOld);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -486,11 +486,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should decode latitude from numeric digits (0-9)", () => {
|
it("should decode latitude from numeric digits (0-9)", () => {
|
||||||
const data = "CALL>123456:`c.l+@&'/'\"G:}";
|
const data = "CALL>123456:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(12 + 34.56 / 60, 3);
|
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)", () => {
|
it("should decode latitude from letter digits (A-J)", () => {
|
||||||
const data = "CALL>ABC0EF:`c.l+@&'/'\"G:}";
|
const data = "CALL>ABC0EF:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(1 + 20.45 / 60, 3);
|
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", () => {
|
it("should decode latitude with mixed digits and letters", () => {
|
||||||
const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}";
|
const data = "CALL>4AB2DE:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
|
expect(decoded.position.latitude).toBeCloseTo(40 + 12.34 / 60, 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -522,11 +522,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should decode latitude for southern hemisphere", () => {
|
it("should decode latitude for southern hemisphere", () => {
|
||||||
const data = "CALL>4A0P0U:`c.l+@&'/'\"G:}";
|
const data = "CALL>4A0P0U:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.latitude).toBeLessThan(0);
|
expect(decoded.position.latitude).toBeLessThan(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -536,11 +536,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should decode longitude from information field", () => {
|
it("should decode longitude from information field", () => {
|
||||||
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
|
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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);
|
||||||
@@ -550,11 +550,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should handle eastern hemisphere longitude", () => {
|
it("should handle eastern hemisphere longitude", () => {
|
||||||
const data = "CALL>4ABPDE:`c.l+@&'/'\"G:}";
|
const data = "CALL>4ABPDE:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(typeof decoded.position.longitude).toBe("number");
|
expect(typeof decoded.position.longitude).toBe("number");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -562,11 +562,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should handle longitude offset +100", () => {
|
it("should handle longitude offset +100", () => {
|
||||||
const data = "CALL>4ABCDP:`c.l+@&'/'\"G:}";
|
const data = "CALL>4ABCDP:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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 +581,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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 +596,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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 +611,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.speed).toBeUndefined();
|
expect(decoded.position.speed).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -619,11 +619,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should not include zero or 360+ course in result", () => {
|
it("should not include zero or 360+ course in result", () => {
|
||||||
const data = "CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}";
|
const data = "CALL>4ABCDE:`c.l\x1c\x1c\x1c/>}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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 +640,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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();
|
||||||
@@ -658,7 +658,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
|
expect(decoded.position.altitude).toBeCloseTo(1234 * 0.3048, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -670,7 +670,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
if (decoded.position.comment?.startsWith("}")) {
|
if (decoded.position.comment?.startsWith("}")) {
|
||||||
expect(decoded.position.altitude).toBeDefined();
|
expect(decoded.position.altitude).toBeDefined();
|
||||||
}
|
}
|
||||||
@@ -680,11 +680,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should prefer /A= format over base-91 when both present", () => {
|
it("should prefer /A= format over base-91 when both present", () => {
|
||||||
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}}!!!/A=005000";
|
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}}!!!/A=005000";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
|
expect(decoded.position.altitude).toBeCloseTo(5000 * 0.3048, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -692,11 +692,11 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should handle comment without altitude", () => {
|
it("should handle comment without altitude", () => {
|
||||||
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Just a comment";
|
const data = "CALL>4ABCDE:`c.l+@&'/'\"G:}Just a comment";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
@@ -707,39 +707,34 @@ describe("Frame.decodeMicE", () => {
|
|||||||
it("should decode message type M0 (Off Duty)", () => {
|
it("should decode message type M0 (Off Duty)", () => {
|
||||||
const data = "CALL>012345:`c.l+@&'/'\"G:}";
|
const data = "CALL>012345:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.micE?.messageType).toBe("M0: Off Duty");
|
expect(decoded.messageType).toBe("M0: Off Duty");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should decode message type M7 (Emergency)", () => {
|
it("should decode message type M7 (Emergency)", () => {
|
||||||
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
|
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.micE?.messageType).toBeDefined();
|
expect(decoded.messageType).toBeDefined();
|
||||||
expect(typeof decoded.micE?.messageType).toBe("string");
|
expect(typeof decoded.messageType).toBe("string");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should decode standard vs custom message indicator", () => {
|
it("should decode standard vs custom message indicator", () => {
|
||||||
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
|
const data = "CALL>ABCDEF:`c.l+@&'/'\"G:}";
|
||||||
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();
|
||||||
|
|
||||||
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();
|
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");
|
expect(decoded.position.comment).toContain("This is a test comment");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -763,7 +758,7 @@ describe("Frame.decodeMicE", () => {
|
|||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.position.comment).toBeDefined();
|
expect(decoded.position.comment).toBeDefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -799,8 +794,10 @@ describe("Frame.decodeMicE", () => {
|
|||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
|
|
||||||
expect(() => frame.decode()).not.toThrow();
|
expect(() => frame.decode()).not.toThrow();
|
||||||
const decoded = frame.decode() as Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
expect(decoded === null || decoded?.type === "position").toBe(true);
|
expect(decoded === null || decoded?.type === DataType.MicECurrent).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -809,37 +806,21 @@ describe("Frame.decodeMicE", () => {
|
|||||||
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 Payload;
|
const decoded = frame.decode() as MicEPayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe("position");
|
expect(decoded?.type).toBe(DataType.MicECurrent);
|
||||||
|
|
||||||
if (decoded && decoded.type === "position") {
|
if (decoded && decoded.type === DataType.MicECurrent) {
|
||||||
expect(decoded.messaging).toBe(true);
|
|
||||||
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();
|
||||||
expect(decoded.micE).toBeDefined();
|
|
||||||
|
|
||||||
expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90);
|
expect(Math.abs(decoded.position.latitude)).toBeLessThanOrEqual(90);
|
||||||
expect(Math.abs(decoded.position.longitude)).toBeLessThanOrEqual(180);
|
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", () => {
|
describe("Packet dissection with sections", () => {
|
||||||
@@ -856,19 +837,19 @@ 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);
|
||||||
@@ -878,12 +859,12 @@ describe("Packet dissection with sections", () => {
|
|||||||
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
|
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const result = frame.decode(true) as {
|
const result = frame.decode(true) as {
|
||||||
payload: Payload;
|
payload: PositionPayload;
|
||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload).not.toBeNull();
|
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).toBeDefined();
|
||||||
expect(result.structure?.length).toBeGreaterThan(0);
|
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", () => {
|
it("should not emit sections when emitSections is false or omitted", () => {
|
||||||
const data = "CALL>APRS:!4903.50N/07201.75W-Test";
|
const data = "CALL>APRS:!4903.50N/07201.75W-Test";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const result = frame.decode() as Payload;
|
const result = frame.decode() as PositionPayload;
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.type).toBe("position");
|
expect(result?.type).toBe(DataType.PositionNoTimestampNoMessaging);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
expect((result as any).sections).toBeUndefined();
|
expect((result as any).sections).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -912,11 +893,13 @@ describe("Packet dissection with sections", () => {
|
|||||||
const data = "CALL>APRS:@092345z4903.50N/07201.75W>";
|
const data = "CALL>APRS:@092345z4903.50N/07201.75W>";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const result = frame.decode(true) as {
|
const result = frame.decode(true) as {
|
||||||
payload: Payload;
|
payload: PositionPayload;
|
||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload?.type).toBe("position");
|
expect(result.payload?.type).toBe(
|
||||||
|
DataType.PositionWithTimestampWithMessaging,
|
||||||
|
);
|
||||||
|
|
||||||
const timestampSection = result.structure?.find(
|
const timestampSection = result.structure?.find(
|
||||||
(s) => s.name === "timestamp",
|
(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 data = 'NOCALL-1>APRS:@092345z/:*E";qZ=OMRC/A=088132Hello World!';
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const result = frame.decode(true) as {
|
const result = frame.decode(true) as {
|
||||||
payload: Payload;
|
payload: PositionPayload;
|
||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload?.type).toBe("position");
|
expect(result.payload?.type).toBe(
|
||||||
|
DataType.PositionWithTimestampWithMessaging,
|
||||||
|
);
|
||||||
|
|
||||||
const positionSection = result.structure?.find(
|
const positionSection = result.structure?.find(
|
||||||
(s) => s.name === "position",
|
(s) => s.name === "position",
|
||||||
@@ -956,11 +941,11 @@ describe("Packet dissection with sections", () => {
|
|||||||
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
|
const data = "CALL>APRS:!4903.50N/07201.75W-Test message";
|
||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const result = frame.decode(true) as {
|
const result = frame.decode(true) as {
|
||||||
payload: Payload;
|
payload: PositionPayload;
|
||||||
structure: Dissected;
|
structure: Dissected;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result.payload?.type).toBe("position");
|
expect(result.payload?.type).toBe(DataType.PositionNoTimestampNoMessaging);
|
||||||
const commentSection = result.structure?.find((s) => s.name === "comment");
|
const commentSection = result.structure?.find((s) => s.name === "comment");
|
||||||
expect(commentSection).toBeDefined();
|
expect(commentSection).toBeDefined();
|
||||||
expect(commentSection?.data?.byteLength).toBe("Test message".length);
|
expect(commentSection?.data?.byteLength).toBe("Test message".length);
|
||||||
@@ -975,7 +960,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
const decoded = frame.decode() as MessagePayload;
|
const decoded = frame.decode() as MessagePayload;
|
||||||
|
|
||||||
expect(decoded).not.toBeNull();
|
expect(decoded).not.toBeNull();
|
||||||
expect(decoded?.type).toBe("message");
|
expect(decoded?.type).toBe(DataType.Message);
|
||||||
expect(decoded?.addressee).toBe("KB1ABC-5");
|
expect(decoded?.addressee).toBe("KB1ABC-5");
|
||||||
expect(decoded?.text).toBe("Hello World");
|
expect(decoded?.text).toBe("Hello World");
|
||||||
});
|
});
|
||||||
@@ -989,7 +974,7 @@ describe("Frame.decodeMessage", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
expect(res.payload).not.toBeNull();
|
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 recipientSection = res.structure.find((s) => s.name === "recipient");
|
||||||
const textSection = res.structure.find((s) => s.name === "text");
|
const textSection = res.structure.find((s) => s.name === "text");
|
||||||
expect(recipientSection).toBeDefined();
|
expect(recipientSection).toBeDefined();
|
||||||
@@ -1013,7 +998,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 !== "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 };
|
||||||
@@ -1044,12 +1029,12 @@ 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 !== "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.type).toBe("status");
|
expect(payload.type).toBe(DataType.Status);
|
||||||
|
|
||||||
expect(payload.timestamp).toBeDefined();
|
expect(payload.timestamp).toBeDefined();
|
||||||
expect(payload.timestamp?.day).toBe(12);
|
expect(payload.timestamp?.day).toBe(12);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
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 +9,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,7 +22,7 @@ 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");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { Frame } from "../src/frame";
|
import { Frame } from "../src/frame";
|
||||||
import { WeatherPayload } from "../src/frame.types";
|
import { DataType, WeatherPayload } from "../src/frame.types";
|
||||||
import { Dissected } from "@hamradio/packet";
|
import { Dissected } from "@hamradio/packet";
|
||||||
|
|
||||||
describe("Frame decode - Weather", () => {
|
describe("Frame decode - Weather", () => {
|
||||||
@@ -9,7 +9,7 @@ describe("Frame decode - Weather", () => {
|
|||||||
const frame = Frame.fromString(data);
|
const frame = Frame.fromString(data);
|
||||||
const payload = frame.decode() as WeatherPayload;
|
const payload = frame.decode() as WeatherPayload;
|
||||||
expect(payload).not.toBeNull();
|
expect(payload).not.toBeNull();
|
||||||
expect(payload.type).toBe("weather");
|
expect(payload.type).toBe(DataType.WeatherReportNoPosition);
|
||||||
expect(payload.timestamp).toBeDefined();
|
expect(payload.timestamp).toBeDefined();
|
||||||
expect(payload.windDirection).toBe(180);
|
expect(payload.windDirection).toBe(180);
|
||||||
expect(payload.windSpeed).toBe(10);
|
expect(payload.windSpeed).toBe(10);
|
||||||
|
|||||||
Reference in New Issue
Block a user