Files
hamview/ui/src/services/APRSStream.ts
maze 13afa08e8a
Some checks failed
Test and build / Test and lint (push) Failing after 36s
Test and build / Build collector (push) Failing after 43s
Test and build / Build receiver (push) Failing after 42s
Checkpoint
2026-03-05 15:38:18 +01:00

118 lines
2.7 KiB
TypeScript

import { BaseStream } from './Stream';
export interface APRSMessage {
topic: string;
receivedAt: Date;
raw: string;
radioName?: string;
}
interface APRSJsonEnvelope {
raw?: string;
payload?: string;
payloadBase64?: string;
rawBase64?: string;
Raw?: string;
Payload?: string;
Time?: string;
time?: string;
timestamp?: string;
}
const fromBase64 = (value: string): string => {
return atob(value);
};
const pickString = (obj: Record<string, unknown>, keys: string[]): string | undefined => {
for (const key of keys) {
const value = obj[key];
if (typeof value === 'string' && value.trim().length > 0) {
return value;
}
}
return undefined;
};
export class APRSStream extends BaseStream {
constructor(autoConnect = false) {
super({}, autoConnect);
}
protected decodeMessage(topic: string, payload: Uint8Array): APRSMessage {
const text = new TextDecoder().decode(payload).trim();
const radioName = this.extractRadioNameFromTopic(topic);
if (!text.startsWith('{')) {
return {
topic,
receivedAt: new Date(),
raw: text,
radioName,
};
}
const envelope = JSON.parse(text) as APRSJsonEnvelope;
const record = envelope as unknown as Record<string, unknown>;
const raw = this.extractRawFrame(record);
const receivedAt = this.extractReceivedAt(record);
return {
topic,
receivedAt,
raw,
radioName,
};
}
private extractRawFrame(envelope: Record<string, unknown>): string {
const rawFrame = pickString(envelope, ['raw', 'payload']);
if (rawFrame && rawFrame.includes('>')) {
return rawFrame;
}
const maybeBase64 = pickString(envelope, ['Raw', 'payloadBase64', 'rawBase64', 'Payload']);
if (maybeBase64) {
const decoded = fromBase64(maybeBase64);
if (decoded.includes('>')) {
return decoded;
}
}
if (rawFrame) {
return rawFrame;
}
throw new Error('APRSStream: invalid payload envelope, no APRS frame found');
}
private extractReceivedAt(envelope: Record<string, unknown>): Date {
const timeValue = pickString(envelope, ['time', 'Time', 'timestamp']);
if (!timeValue) {
return new Date();
}
const parsed = new Date(timeValue);
if (Number.isNaN(parsed.getTime())) {
return new Date();
}
return parsed;
}
private extractRadioNameFromTopic(topic: string): string | undefined {
const parts = topic.split('/');
if (parts.length >= 3 && parts[0] === 'aprs' && parts[1] === 'packet') {
try {
return atob(parts[2]);
} catch (error) {
console.warn('Failed to decode radio name from topic:', topic, error);
}
}
return undefined;
}
}
export default APRSStream;