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, 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; const raw = this.extractRawFrame(record); const receivedAt = this.extractReceivedAt(record); return { topic, receivedAt, raw, radioName, }; } private extractRawFrame(envelope: Record): 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): 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;