Checkpoint
This commit is contained in:
117
ui/src/services/APRSStream.ts
Normal file
117
ui/src/services/APRSStream.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user