Initial release

This commit is contained in:
2026-03-11 17:24:57 +01:00
commit 5d537975e9
17 changed files with 6635 additions and 0 deletions

1109
src/frame.ts Normal file

File diff suppressed because it is too large Load Diff

325
src/frame.types.ts Normal file
View File

@@ -0,0 +1,325 @@
import { PacketSegment, PacketStructure } from "./parser.types";
export interface IAddress {
call: string;
ssid: string;
isRepeated: boolean;
}
export interface IFrame {
source: IAddress;
destination: IAddress;
path: IAddress[];
payload: string;
}
// APRS Data Type Identifiers (first character of payload)
export const DataTypeIdentifier = {
// Position Reports
PositionNoTimestampNoMessaging: '!',
PositionNoTimestampWithMessaging: '=',
PositionWithTimestampNoMessaging: '/',
PositionWithTimestampWithMessaging: '@',
// Mic-E
MicECurrent: '`',
MicEOld: "'",
// Messages and Bulletins
Message: ':',
// Objects and Items
Object: ';',
Item: ')',
// Status
Status: '>',
// Query
Query: '?',
// Telemetry
TelemetryData: 'T',
// Weather
WeatherReportNoPosition: '_',
// Raw GPS Data
RawGPS: '$',
// Station Capabilities
StationCapabilities: '<',
// User-Defined
UserDefined: '{',
// Third-Party Traffic
ThirdParty: '}',
// Invalid/Test Data
InvalidOrTest: ',',
} as const;
export type DataTypeIdentifier = typeof DataTypeIdentifier[keyof typeof DataTypeIdentifier];
export interface ISymbol {
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
ambiguity?: number; // Position ambiguity (0-4)
altitude?: number; // Meters
speed?: number; // Speed in knots/kmh depending on source
course?: number; // Course in degrees
symbol?: ISymbol;
comment?: string;
toString(): string; // Return combined position representation (e.g., "lat,lon,alt")
toCompressed?(): CompressedPosition; // Optional method to convert to compressed format
distanceTo?(other: IPosition): number; // Optional method to calculate distance to another position
}
export interface ITimestamp {
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
}
// Position Report Payload
export interface PositionPayload {
type: 'position';
timestamp?: ITimestamp;
position: IPosition;
messaging: boolean; // Whether APRS messaging is enabled
micE?: {
messageType?: string;
isStandard?: boolean;
};
sections?: PacketSegment[];
}
// Compressed Position Format
export interface CompressedPosition {
latitude: number;
longitude: number;
symbol: {
table: string;
code: string;
};
course?: number; // Degrees
speed?: number; // Knots
range?: number; // Miles
altitude?: number; // Feet
radioRange?: number; // Miles
compression: 'old' | 'current';
}
// Mic-E Payload (compressed in destination address)
export interface MicEPayload {
type: 'mic-e';
position: IPosition;
course?: number;
speed?: number;
altitude?: number;
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
}
// Bulletin/Announcement (variant of message)
export interface BulletinPayload {
type: 'bulletin';
bulletinId: string; // Bulletin identifier (BLN#)
text: string;
group?: string; // Optional group bulletin
}
// Object Payload
export interface ObjectPayload {
type: 'object';
name: string; // 9 character object name
timestamp: ITimestamp;
alive: boolean; // True if object is active, false if killed
position: IPosition;
course?: number;
speed?: number;
}
// Item Payload
export interface ItemPayload {
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';
timestamp?: ITimestamp;
text: string;
maidenhead?: string; // Optional Maidenhead grid locator
symbol?: {
table: string;
code: string;
};
}
// Query Payload
export interface QueryPayload {
type: 'query';
queryType: string; // e.g., 'APRSD', 'APRST', 'PING'
target?: string; // Target callsign or area
}
// Telemetry Data Payload
export interface TelemetryDataPayload {
type: 'telemetry-data';
sequence: number;
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
}
// Telemetry Unit/Label
export interface TelemetryUnitPayload {
type: 'telemetry-units';
units: string[]; // Units for each parameter
}
// Telemetry Coefficients
export interface TelemetryCoefficientsPayload {
type: 'telemetry-coefficients';
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
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
}
// Raw GPS Payload (NMEA sentences)
export interface RawGPSPayload {
type: 'raw-gps';
sentence: string; // Raw NMEA sentence
}
// Station Capabilities Payload
export interface StationCapabilitiesPayload {
type: 'capabilities';
capabilities: string[];
}
// User-Defined Payload
export interface UserDefinedPayload {
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
}
// DF Report Payload
export interface DFReportPayload {
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
directivity?: string; // Antenna directivity pattern
}
export interface BasePayload {
type: string;
}
// 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
);
// Extended Frame with decoded payload
export interface DecodedFrame extends IFrame {
decoded?: Payload;
structure?: PacketStructure; // Routing and other frame-level sections
}

57
src/index.ts Normal file
View File

@@ -0,0 +1,57 @@
export {
Frame,
Address,
Timestamp,
} from "./frame";
export {
type IAddress,
type IFrame,
DataTypeIdentifier,
} from "./frame.types";
export {
type ISymbol,
type IPosition,
type ITimestamp,
type PositionPayload,
type CompressedPosition,
type MicEPayload,
type MessagePayload,
type BulletinPayload,
type ObjectPayload,
type ItemPayload,
type StatusPayload,
type QueryPayload,
type TelemetryDataPayload,
type TelemetryParameterPayload,
type TelemetryUnitPayload,
type TelemetryCoefficientsPayload,
type TelemetryBitSensePayload,
type WeatherPayload,
type RawGPSPayload,
type StationCapabilitiesPayload,
type UserDefinedPayload,
type ThirdPartyPayload,
type DFReportPayload,
type BasePayload,
type Payload,
type DecodedFrame,
} from "./frame.types";
export {
base91ToNumber,
knotsToKmh,
kmhToKnots,
feetToMeters,
metersToFeet,
celsiusToFahrenheit,
fahrenheitToCelsius,
} from "./parser";
export {
type PacketStructure,
type PacketSegment,
type PacketField,
type PacketFieldBit,
FieldType,
} from "./parser.types";

91
src/parser.ts Normal file
View File

@@ -0,0 +1,91 @@
/**
* Decode a Base91 encoded string to a number.
* Base91 uses ASCII characters 33-123 (! to {) for encoding.
* Each character represents a digit in base 91.
*
* @param str Base91 encoded string (typically 4 characters for 32-bit values in APRS)
* @returns Decoded number value
*/
export const base91ToNumber = (str: string): number => {
let value = 0;
const base = 91;
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
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})`);
}
value = value * base + digit;
}
return value;
}
/* Conversions from Freedom Units to whatever the rest of the world uses and understands. */
const KNOTS_TO_KMH = 1.852;
const FEET_TO_METERS = 0.3048;
const FAHRENHEIT_TO_CELSIUS_OFFSET = 32;
/**
* Convert speed from knots to kilometers per hour.
*
* @param knots number of knots
* @returns equivalent speed in kilometers per hour
*/
export const knotsToKmh = (knots: number): number => {
return knots * KNOTS_TO_KMH;
}
/**
* Convert speed from kilometers per hour to knots.
*
* @param kmh speed in kilometers per hour
* @returns equivalent speed in knots
*/
export const kmhToKnots = (kmh: number): number => {
return kmh / KNOTS_TO_KMH;
}
/**
* Convert altitude from feet to meters.
*
* @param feet altitude in feet
* @returns equivalent altitude in meters
*/
export const feetToMeters = (feet: number): number => {
return feet * FEET_TO_METERS;
}
/**
* Convert altitude from meters to feet.
*
* @param meters altitude in meters
* @returns equivalent altitude in feet
*/
export const metersToFeet = (meters: number): number => {
return meters / FEET_TO_METERS;
}
/**
* Convert temperature from Celsius to Fahrenheit.
*
* @param celsius temperature in Celsius
* @returns equivalent temperature in Fahrenheit
*/
export const celsiusToFahrenheit = (celsius: number): number => {
return (celsius * 9/5) + FAHRENHEIT_TO_CELSIUS_OFFSET;
}
/**
* Convert temperature from Fahrenheit to Celsius.
*
* @param fahrenheit temperature in Fahrenheit
* @returns equivalent temperature in Celsius
*/
export const fahrenheitToCelsius = (fahrenheit: number): number => {
return (fahrenheit - FAHRENHEIT_TO_CELSIUS_OFFSET) * 5/9;
}

37
src/parser.types.ts Normal file
View File

@@ -0,0 +1,37 @@
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
}

72
src/position.ts Normal file
View File

@@ -0,0 +1,72 @@
import { IPosition, ISymbol } from "./frame.types";
export class Symbol implements ISymbol {
table: string; // Symbol table identifier
code: string; // Symbol code
constructor(table: string, code?: string) {
if (code === undefined) {
if (table.length === 2) {
this.code = table[1];
this.table = table[0];
} else {
throw new Error(`Invalid symbol format: '${table}' (expected 2 characters if code is not provided)`);
}
} else {
this.table = table;
this.code = code;
}
}
public toString(): string {
return `${this.table}${this.code}`;
}
}
export class Position implements IPosition {
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
symbol?: Symbol;
comment?: string;
constructor(data: Partial<IPosition>) {
this.latitude = data.latitude ?? 0;
this.longitude = data.longitude ?? 0;
this.ambiguity = data.ambiguity;
this.altitude = data.altitude;
this.speed = data.speed;
this.course = data.course;
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);
}
this.comment = data.comment;
}
public toString(): string {
const latStr = this.latitude.toFixed(5);
const lonStr = this.longitude.toFixed(5);
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 φ1 = this.latitude * Math.PI / 180;
const φ2 = other.latitude * Math.PI / 180;
const Δφ = (other.latitude - this.latitude) * Math.PI / 180;
const Δλ = (other.longitude - this.longitude) * Math.PI / 180;
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // Distance in meters
}
}