Stricter decoding

This commit is contained in:
2026-03-15 20:21:26 +01:00
parent 4669783b67
commit e0d4844c5b
12 changed files with 1241 additions and 4188 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,83 @@
import { PacketSegment, PacketStructure } from "./parser.types";
export interface IAddress {
call: string;
ssid: string;
call: string;
ssid: string;
isRepeated: boolean;
}
export interface IFrame {
source: IAddress;
source: IAddress;
destination: IAddress;
path: IAddress[];
payload: string;
path: IAddress[];
payload: string;
}
// APRS Data Type Identifiers (first character of payload)
export const DataTypeIdentifier = {
// Position Reports
PositionNoTimestampNoMessaging: '!',
PositionNoTimestampWithMessaging: '=',
PositionWithTimestampNoMessaging: '/',
PositionWithTimestampWithMessaging: '@',
PositionNoTimestampNoMessaging: "!",
PositionNoTimestampWithMessaging: "=",
PositionWithTimestampNoMessaging: "/",
PositionWithTimestampWithMessaging: "@",
// Mic-E
MicECurrent: '`',
MicECurrent: "`",
MicEOld: "'",
// Messages and Bulletins
Message: ':',
Message: ":",
// Objects and Items
Object: ';',
Item: ')',
Object: ";",
Item: ")",
// Status
Status: '>',
Status: ">",
// Query
Query: '?',
Query: "?",
// Telemetry
TelemetryData: 'T',
TelemetryData: "T",
// Weather
WeatherReportNoPosition: '_',
WeatherReportNoPosition: "_",
// Raw GPS Data
RawGPS: '$',
RawGPS: "$",
// Station Capabilities
StationCapabilities: '<',
StationCapabilities: "<",
// User-Defined
UserDefined: '{',
UserDefined: "{",
// Third-Party Traffic
ThirdParty: '}',
ThirdParty: "}",
// Invalid/Test Data
InvalidOrTest: ',',
InvalidOrTest: ",",
} as const;
export type DataTypeIdentifier = typeof DataTypeIdentifier[keyof typeof DataTypeIdentifier];
export type DataTypeIdentifier =
(typeof DataTypeIdentifier)[keyof typeof DataTypeIdentifier];
export interface ISymbol {
table: string; // Symbol table identifier
code: string; // Symbol code
table: string; // Symbol table identifier
code: string; // Symbol code
toString(): string; // Return combined symbol representation (e.g., "tablecode")
}
// Position data common to multiple formats
export interface IPosition {
latitude: number; // Decimal degrees
longitude: number; // Decimal degrees
latitude: number; // Decimal degrees
longitude: number; // Decimal degrees
ambiguity?: number; // Position ambiguity (0-4)
altitude?: number; // Meters
speed?: number; // Speed in knots/kmh depending on source
course?: number; // Course in degrees
altitude?: number; // Meters
speed?: number; // Speed in knots/kmh depending on source
course?: number; // Course in degrees
symbol?: ISymbol;
comment?: string;
@@ -86,22 +87,22 @@ export interface IPosition {
}
export interface ITimestamp {
day?: number; // Day of month (DHM format)
month?: number; // Month (MDHM format)
day?: number; // Day of month (DHM format)
month?: number; // Month (MDHM format)
hours: number;
minutes: number;
seconds?: number;
format: 'DHM' | 'HMS' | 'MDHM'; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute
zulu?: boolean; // Is UTC/Zulu time
toDate(): Date; // Convert to Date object respecting timezone
format: "DHM" | "HMS" | "MDHM"; // Day-Hour-Minute, Hour-Minute-Second, Month-Day-Hour-Minute
zulu?: boolean; // Is UTC/Zulu time
toDate(): Date; // Convert to Date object respecting timezone
}
// Position Report Payload
export interface PositionPayload {
type: 'position';
type: "position";
timestamp?: ITimestamp;
position: IPosition;
messaging: boolean; // Whether APRS messaging is enabled
messaging: boolean; // Whether APRS messaging is enabled
micE?: {
messageType?: string;
isStandard?: boolean;
@@ -117,50 +118,50 @@ export interface CompressedPosition {
table: string;
code: string;
};
course?: number; // Degrees
speed?: number; // Knots
range?: number; // Miles
altitude?: number; // Feet
course?: number; // Degrees
speed?: number; // Knots
range?: number; // Miles
altitude?: number; // Feet
radioRange?: number; // Miles
compression: 'old' | 'current';
compression: "old" | "current";
}
// Mic-E Payload (compressed in destination address)
export interface MicEPayload {
type: 'mic-e';
type: "mic-e";
position: IPosition;
course?: number;
speed?: number;
altitude?: number;
messageType?: string; // Standard Mic-E message
telemetry?: number[]; // Optional telemetry channels
messageType?: string; // Standard Mic-E message
telemetry?: number[]; // Optional telemetry channels
status?: string;
}
// Message Payload
export interface MessagePayload {
type: 'message';
addressee: string; // 9 character padded callsign
text: string; // Message text
messageNumber?: string; // Message ID for acknowledgment
ack?: string; // Acknowledgment of message ID
reject?: string; // Rejection of message ID
type: "message";
addressee: string; // 9 character padded callsign
text: string; // Message text
messageNumber?: string; // Message ID for acknowledgment
ack?: string; // Acknowledgment of message ID
reject?: string; // Rejection of message ID
}
// Bulletin/Announcement (variant of message)
export interface BulletinPayload {
type: 'bulletin';
bulletinId: string; // Bulletin identifier (BLN#)
type: "bulletin";
bulletinId: string; // Bulletin identifier (BLN#)
text: string;
group?: string; // Optional group bulletin
group?: string; // Optional group bulletin
}
// Object Payload
export interface ObjectPayload {
type: 'object';
name: string; // 9 character object name
type: "object";
name: string; // 9 character object name
timestamp: ITimestamp;
alive: boolean; // True if object is active, false if killed
alive: boolean; // True if object is active, false if killed
position: IPosition;
course?: number;
speed?: number;
@@ -168,15 +169,15 @@ export interface ObjectPayload {
// Item Payload
export interface ItemPayload {
type: 'item';
name: string; // 3-9 character item name
alive: boolean; // True if item is active, false if killed
type: "item";
name: string; // 3-9 character item name
alive: boolean; // True if item is active, false if killed
position: IPosition;
}
// Status Payload
export interface StatusPayload {
type: 'status';
type: "status";
timestamp?: ITimestamp;
text: string;
maidenhead?: string; // Optional Maidenhead grid locator
@@ -188,106 +189,106 @@ export interface StatusPayload {
// Query Payload
export interface QueryPayload {
type: 'query';
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
target?: string; // Target callsign or area
type: "query";
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
target?: string; // Target callsign or area
}
// Telemetry Data Payload
export interface TelemetryDataPayload {
type: 'telemetry-data';
type: "telemetry-data";
sequence: number;
analog: number[]; // Up to 5 analog channels
digital: number; // 8-bit digital value
analog: number[]; // Up to 5 analog channels
digital: number; // 8-bit digital value
}
// Telemetry Parameter Names
export interface TelemetryParameterPayload {
type: 'telemetry-parameters';
names: string[]; // Parameter names
type: "telemetry-parameters";
names: string[]; // Parameter names
}
// Telemetry Unit/Label
export interface TelemetryUnitPayload {
type: 'telemetry-units';
units: string[]; // Units for each parameter
type: "telemetry-units";
units: string[]; // Units for each parameter
}
// Telemetry Coefficients
export interface TelemetryCoefficientsPayload {
type: 'telemetry-coefficients';
type: "telemetry-coefficients";
coefficients: {
a: number[]; // a coefficients
b: number[]; // b coefficients
c: number[]; // c coefficients
a: number[]; // a coefficients
b: number[]; // b coefficients
c: number[]; // c coefficients
};
}
// Telemetry Bit Sense/Project Name
export interface TelemetryBitSensePayload {
type: 'telemetry-bitsense';
sense: number; // 8-bit sense value
type: "telemetry-bitsense";
sense: number; // 8-bit sense value
projectName?: string;
}
// Weather Report Payload
export interface WeatherPayload {
type: 'weather';
timestamp?: ITimestamp;
position?: IPosition;
windDirection?: number; // Degrees
windSpeed?: number; // MPH
windGust?: number; // MPH
temperature?: number; // Fahrenheit
rainLastHour?: number; // Hundredths of inch
rainLast24Hours?: number; // Hundredths of inch
rainSinceMidnight?: number; // Hundredths of inch
humidity?: number; // Percent
pressure?: number; // Tenths of millibar
luminosity?: number; // Watts per square meter
snowfall?: number; // Inches
rawRain?: number; // Raw rain counter
software?: string; // Weather software type
weatherUnit?: string; // Weather station type
type: "weather";
timestamp?: ITimestamp;
position?: IPosition;
windDirection?: number; // Degrees
windSpeed?: number; // MPH
windGust?: number; // MPH
temperature?: number; // Fahrenheit
rainLastHour?: number; // Hundredths of inch
rainLast24Hours?: number; // Hundredths of inch
rainSinceMidnight?: number; // Hundredths of inch
humidity?: number; // Percent
pressure?: number; // Tenths of millibar
luminosity?: number; // Watts per square meter
snowfall?: number; // Inches
rawRain?: number; // Raw rain counter
software?: string; // Weather software type
weatherUnit?: string; // Weather station type
}
// Raw GPS Payload (NMEA sentences)
export interface RawGPSPayload {
type: 'raw-gps';
sentence: string; // Raw NMEA sentence
type: "raw-gps";
sentence: string; // Raw NMEA sentence
}
// Station Capabilities Payload
export interface StationCapabilitiesPayload {
type: 'capabilities';
type: "capabilities";
capabilities: string[];
}
// User-Defined Payload
export interface UserDefinedPayload {
type: 'user-defined';
type: "user-defined";
userPacketType: string;
data: string;
}
// Third-Party Traffic Payload
export interface ThirdPartyPayload {
type: 'third-party';
header: string; // Source path of third-party packet
payload: string; // Nested APRS packet
type: "third-party";
header: string; // Source path of third-party packet
payload: string; // Nested APRS packet
}
// DF Report Payload
export interface DFReportPayload {
type: 'df-report';
type: "df-report";
timestamp?: ITimestamp;
position: IPosition;
course?: number;
bearing?: number; // Direction finding bearing
quality?: number; // Signal quality
strength?: number; // Signal strength
height?: number; // Antenna height
gain?: number; // Antenna gain
bearing?: number; // Direction finding bearing
quality?: number; // Signal quality
strength?: number; // Signal strength
height?: number; // Antenna height
gain?: number; // Antenna gain
directivity?: string; // Antenna directivity pattern
}
@@ -296,30 +297,31 @@ export interface BasePayload {
}
// Union type for all decoded payload types
export type Payload = BasePayload & (
| PositionPayload
| MicEPayload
| MessagePayload
| BulletinPayload
| ObjectPayload
| ItemPayload
| StatusPayload
| QueryPayload
| TelemetryDataPayload
| TelemetryParameterPayload
| TelemetryUnitPayload
| TelemetryCoefficientsPayload
| TelemetryBitSensePayload
| WeatherPayload
| RawGPSPayload
| StationCapabilitiesPayload
| UserDefinedPayload
| ThirdPartyPayload
| DFReportPayload
);
export type Payload = BasePayload &
(
| PositionPayload
| MicEPayload
| MessagePayload
| BulletinPayload
| ObjectPayload
| ItemPayload
| StatusPayload
| QueryPayload
| TelemetryDataPayload
| TelemetryParameterPayload
| TelemetryUnitPayload
| TelemetryCoefficientsPayload
| TelemetryBitSensePayload
| WeatherPayload
| RawGPSPayload
| StationCapabilitiesPayload
| UserDefinedPayload
| ThirdPartyPayload
| DFReportPayload
);
// Extended Frame with decoded payload
export interface DecodedFrame extends IFrame {
decoded?: Payload;
decoded?: Payload;
structure?: PacketStructure; // Routing and other frame-level sections
}

View File

@@ -1,14 +1,6 @@
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, DataTypeIdentifier } from "./frame.types";
export {
type ISymbol,
@@ -48,10 +40,3 @@ export {
celsiusToFahrenheit,
fahrenheitToCelsius,
} from "./parser";
export {
type PacketStructure,
type PacketSegment,
type PacketField,
type PacketFieldBit,
FieldType,
} from "./parser.types";

View File

@@ -15,14 +15,16 @@ export const base91ToNumber = (str: string): number => {
const digit = charCode - 33; // Base91 uses chars 33-123 (! to {)
if (digit < 0 || digit >= base) {
throw new Error(`Invalid Base91 character: '${str[i]}' (code ${charCode})`);
throw new Error(
`Invalid Base91 character: '${str[i]}' (code ${charCode})`,
);
}
value = value * base + digit;
}
return value;
}
};
/* Conversions from Freedom Units to whatever the rest of the world uses and understands. */
@@ -38,7 +40,7 @@ const FAHRENHEIT_TO_CELSIUS_OFFSET = 32;
*/
export const knotsToKmh = (knots: number): number => {
return knots * KNOTS_TO_KMH;
}
};
/**
* Convert speed from kilometers per hour to knots.
@@ -48,7 +50,7 @@ export const knotsToKmh = (knots: number): number => {
*/
export const kmhToKnots = (kmh: number): number => {
return kmh / KNOTS_TO_KMH;
}
};
/**
* Convert altitude from feet to meters.
@@ -58,7 +60,7 @@ export const kmhToKnots = (kmh: number): number => {
*/
export const feetToMeters = (feet: number): number => {
return feet * FEET_TO_METERS;
}
};
/**
* Convert altitude from meters to feet.
@@ -68,7 +70,7 @@ export const feetToMeters = (feet: number): number => {
*/
export const metersToFeet = (meters: number): number => {
return meters / FEET_TO_METERS;
}
};
/**
* Convert temperature from Celsius to Fahrenheit.
@@ -77,8 +79,8 @@ export const metersToFeet = (meters: number): number => {
* @returns equivalent temperature in Fahrenheit
*/
export const celsiusToFahrenheit = (celsius: number): number => {
return (celsius * 9/5) + FAHRENHEIT_TO_CELSIUS_OFFSET;
}
return (celsius * 9) / 5 + FAHRENHEIT_TO_CELSIUS_OFFSET;
};
/**
* Convert temperature from Fahrenheit to Celsius.
@@ -87,5 +89,5 @@ export const celsiusToFahrenheit = (celsius: number): number => {
* @returns equivalent temperature in Celsius
*/
export const fahrenheitToCelsius = (fahrenheit: number): number => {
return (fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5/9;
}
return ((fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5) / 9;
};

View File

@@ -1,37 +0,0 @@
export enum FieldType {
BITS = 0,
UINT8 = 1,
UINT16_LE = 2,
UINT16_BE = 3,
UINT32_LE = 4,
UINT32_BE = 5,
BYTES = 6, // 8-bits per value
WORDS = 7, // 16-bits per value
DWORDS = 8, // 32-bits per value
QWORDS = 9, // 64-bits per value
STRING = 10,
C_STRING = 11, // Null-terminated string
CHAR = 12, // Single ASCII character
}
// Interface for the parsed packet segments, used for debugging and testing.
export type PacketStructure = PacketSegment[];
export interface PacketSegment {
name: string;
data: Uint8Array;
fields: PacketField[];
}
export interface PacketField {
type: FieldType;
size: number; // Size in bytes
name?: string;
bits?: PacketFieldBit[]; // Only for bit fields in FieldType.BITS
value?: any; // Optional decoded value
}
export interface PacketFieldBit {
name: string;
size: number; // Size in bits
}

View File

@@ -1,8 +1,8 @@
import { IPosition, ISymbol } from "./frame.types";
export class Symbol implements ISymbol {
table: string; // Symbol table identifier
code: string; // Symbol code
table: string; // Symbol table identifier
code: string; // Symbol code
constructor(table: string, code?: string) {
if (code === undefined) {
@@ -10,7 +10,9 @@ export class Symbol implements ISymbol {
this.code = table[1];
this.table = table[0];
} else {
throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`);
throw new Error(
`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`,
);
}
} else {
this.table = table;
@@ -24,12 +26,12 @@ export class Symbol implements ISymbol {
}
export class Position implements IPosition {
latitude: number; // Decimal degrees
longitude: number; // Decimal degrees
latitude: number; // Decimal degrees
longitude: number; // Decimal degrees
ambiguity?: number; // Position ambiguity (0-4)
altitude?: number; // Meters
speed?: number; // Speed in knots/kmh depending on source
course?: number; // Course in degrees
altitude?: number; // Meters
speed?: number; // Speed in knots/kmh depending on source
course?: number; // Course in degrees
symbol?: Symbol;
comment?: string;
@@ -40,7 +42,7 @@ export class Position implements IPosition {
this.altitude = data.altitude;
this.speed = data.speed;
this.course = data.course;
if (typeof data.symbol === 'string') {
if (typeof data.symbol === "string") {
this.symbol = new Symbol(data.symbol);
} else if (data.symbol) {
this.symbol = new Symbol(data.symbol.table, data.symbol.code);
@@ -51,21 +53,21 @@ export class Position implements IPosition {
public toString(): string {
const latStr = this.latitude.toFixed(5);
const lonStr = this.longitude.toFixed(5);
const altStr = this.altitude !== undefined ? `,${this.altitude}m` : '';
const altStr = this.altitude !== undefined ? `,${this.altitude}m` : "";
return `${latStr},${lonStr}${altStr}`;
}
public distanceTo(other: IPosition): number {
const R = 6371e3; // Earth radius in meters
const lat1 = this.latitude * Math.PI / 180;
const lat2 = other.latitude * Math.PI / 180;
const dLat = (other.latitude - this.latitude) * Math.PI / 180;
const dLon = (other.longitude - this.longitude) * Math.PI / 180;
const lat1 = (this.latitude * Math.PI) / 180;
const lat2 = (other.latitude * Math.PI) / 180;
const dLat = ((other.latitude - this.latitude) * Math.PI) / 180;
const dLon = ((other.longitude - this.longitude) * Math.PI) / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}