Added message, object and status decoding
This commit is contained in:
281
src/frame.ts
281
src/frame.ts
@@ -861,10 +861,92 @@ export class Frame implements IFrame {
|
||||
};
|
||||
}
|
||||
|
||||
private decodeMessage(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement message decoding with section emission
|
||||
// When implemented, build structure during parsing like decodePosition does
|
||||
return { payload: null };
|
||||
private decodeMessage(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// Message format: :AAAAAAAAA[ ]:message text
|
||||
// where AAAAAAAAA is a 9-character recipient field (padded with spaces)
|
||||
if (this.payload.length < 2) return { payload: null };
|
||||
|
||||
let offset = 1; // skip ':' data type
|
||||
const segments: PacketSegment[] = withStructure ? [] : [];
|
||||
|
||||
// Attempt to read a 9-char recipient field if present
|
||||
let recipient = '';
|
||||
if (this.payload.length >= offset + 1) {
|
||||
// Try to read up to 9 chars for recipient, but stop early if a ':' separator appears
|
||||
const look = this.payload.substring(offset, Math.min(offset + 9, this.payload.length));
|
||||
const sepIdx = look.indexOf(':');
|
||||
let raw = look;
|
||||
if (sepIdx !== -1) {
|
||||
raw = look.substring(0, sepIdx);
|
||||
} else if (look.length < 9 && this.payload.length >= offset + 9) {
|
||||
// pad to full 9 chars if possible
|
||||
raw = this.payload.substring(offset, offset + 9);
|
||||
} else if (look.length === 9) {
|
||||
raw = look;
|
||||
}
|
||||
|
||||
recipient = raw.trimEnd();
|
||||
if (withStructure) {
|
||||
segments.push({
|
||||
name: 'recipient',
|
||||
data: new TextEncoder().encode(raw),
|
||||
fields: [
|
||||
{ type: FieldType.STRING, name: 'to', size: 9 },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Advance offset past the raw we consumed
|
||||
offset += raw.length;
|
||||
// If there was a ':' immediately after the consumed raw, skip it as separator
|
||||
if (this.payload.charAt(offset) === ':') {
|
||||
offset += 1;
|
||||
} else if (sepIdx !== -1) {
|
||||
// Shouldn't normally happen, but ensure we advance past separator
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// After recipient there is typically a space and a colon separator before the text
|
||||
// Find the first ':' after the recipient (it separates the address field from the text)
|
||||
let textStart = this.payload.indexOf(':', offset);
|
||||
if (textStart === -1) {
|
||||
// No explicit separator; skip any spaces and take remainder as text
|
||||
while (this.payload.charAt(offset) === ' ' && offset < this.payload.length) offset += 1;
|
||||
textStart = offset - 1;
|
||||
}
|
||||
|
||||
let text = '';
|
||||
if (textStart >= 0 && textStart + 1 <= this.payload.length) {
|
||||
text = this.payload.substring(textStart + 1);
|
||||
}
|
||||
|
||||
if (withStructure) {
|
||||
// Emit text section
|
||||
segments.push({
|
||||
name: 'text',
|
||||
data: new TextEncoder().encode(text),
|
||||
fields: [
|
||||
{ type: FieldType.STRING, name: 'text', size: text.length },
|
||||
],
|
||||
});
|
||||
|
||||
const payload: any = {
|
||||
type: 'message',
|
||||
to: recipient || undefined,
|
||||
text,
|
||||
};
|
||||
|
||||
return { payload, segment: segments };
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
type: 'message',
|
||||
to: recipient || undefined,
|
||||
text,
|
||||
};
|
||||
|
||||
return { payload };
|
||||
}
|
||||
|
||||
private decodeObject(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
@@ -989,47 +1071,212 @@ export class Frame implements IFrame {
|
||||
}
|
||||
}
|
||||
|
||||
private decodeItem(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement item decoding with section emission
|
||||
return { payload: null };
|
||||
private decodeItem(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// Item format is similar to Object but name may be 3-9 chars (stored in a 9-char field)
|
||||
// Example: )NNN... where ) is data type, next 9 chars are name, then state char, then timestamp, then position
|
||||
if (this.payload.length < 12) return { payload: null }; // minimal: 1 + 3 + 1 + 7
|
||||
|
||||
let offset = 1; // skip data type identifier ')'
|
||||
const segment: PacketSegment[] = withStructure ? [] : [];
|
||||
|
||||
// Read 9-char name field (pad/truncate as present)
|
||||
const rawName = this.payload.substring(offset, offset + 9);
|
||||
const name = rawName.trimEnd();
|
||||
if (withStructure) {
|
||||
segment.push({
|
||||
name: 'item name',
|
||||
data: new TextEncoder().encode(rawName),
|
||||
fields: [
|
||||
{ type: FieldType.STRING, name: 'name', size: 9 },
|
||||
],
|
||||
});
|
||||
}
|
||||
offset += 9;
|
||||
|
||||
// State character: '*' = alive, '_' = killed
|
||||
const stateChar = this.payload.charAt(offset);
|
||||
if (stateChar !== '*' && stateChar !== '_') {
|
||||
return { payload: null };
|
||||
}
|
||||
const alive = stateChar === '*';
|
||||
if (withStructure) {
|
||||
segment.push({
|
||||
name: 'item state',
|
||||
data: new TextEncoder().encode(stateChar),
|
||||
fields: [
|
||||
{ type: FieldType.CHAR, name: 'State (* alive, _ killed)', size: 1 },
|
||||
],
|
||||
});
|
||||
}
|
||||
offset += 1;
|
||||
|
||||
// Timestamp (7 chars)
|
||||
const timeStr = this.payload.substring(offset, offset + 7);
|
||||
const { timestamp, segment: timestampSection } = this.parseTimestamp(timeStr, withStructure, offset);
|
||||
if (!timestamp) return { payload: null };
|
||||
if (timestampSection) segment.push(timestampSection);
|
||||
offset += 7;
|
||||
|
||||
const positionOffset = offset;
|
||||
const isCompressed = this.isCompressedPosition(this.payload.substring(offset));
|
||||
|
||||
let position: { latitude: number; longitude: number; symbol: any; ambiguity?: number; altitude?: number; comment?: string } | null = null;
|
||||
let consumed = 0;
|
||||
|
||||
if (isCompressed) {
|
||||
const { position: compressed, segment: compressedSection } = this.parseCompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
|
||||
if (!compressed) return { payload: null };
|
||||
|
||||
position = {
|
||||
latitude: compressed.latitude,
|
||||
longitude: compressed.longitude,
|
||||
symbol: compressed.symbol,
|
||||
altitude: compressed.altitude,
|
||||
};
|
||||
consumed = 13;
|
||||
|
||||
if (compressedSection) segment.push(compressedSection);
|
||||
} else {
|
||||
const { position: uncompressed, segment: uncompressedSection } = this.parseUncompressedPosition(this.payload.substring(offset), withStructure, positionOffset);
|
||||
if (!uncompressed) return { payload: null };
|
||||
|
||||
position = {
|
||||
latitude: uncompressed.latitude,
|
||||
longitude: uncompressed.longitude,
|
||||
symbol: uncompressed.symbol,
|
||||
ambiguity: uncompressed.ambiguity,
|
||||
};
|
||||
consumed = 19;
|
||||
|
||||
if (uncompressedSection) segment.push(uncompressedSection);
|
||||
}
|
||||
|
||||
offset += consumed;
|
||||
const comment = this.payload.substring(offset);
|
||||
if (comment) {
|
||||
position.comment = comment;
|
||||
if (withStructure) {
|
||||
segment.push({
|
||||
name: 'Comment',
|
||||
data: new TextEncoder().encode(comment),
|
||||
fields: [
|
||||
{ type: FieldType.STRING, name: 'text', size: comment.length },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
type: 'item',
|
||||
name,
|
||||
alive,
|
||||
position,
|
||||
};
|
||||
|
||||
if (withStructure) {
|
||||
return { payload, segment };
|
||||
}
|
||||
|
||||
return { payload };
|
||||
}
|
||||
|
||||
private decodeStatus(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement status decoding with section emission
|
||||
return { payload: null };
|
||||
private decodeStatus(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// Status payload: optional 7-char timestamp followed by free text.
|
||||
// We'll also detect a trailing Maidenhead locator (4 or 6 chars) and expose it.
|
||||
const offsetBase = 1; // skip data type identifier '>'
|
||||
if (this.payload.length <= offsetBase) return { payload: null };
|
||||
|
||||
let offset = offsetBase;
|
||||
const segments: PacketSegment[] = withStructure ? [] : [];
|
||||
|
||||
// Try parse optional timestamp (7 chars)
|
||||
if (this.payload.length >= offset + 7) {
|
||||
const timeStr = this.payload.substring(offset, offset + 7);
|
||||
const { timestamp, segment: tsSegment } = this.parseTimestamp(timeStr, withStructure, offset);
|
||||
if (timestamp) {
|
||||
offset += 7;
|
||||
if (tsSegment) segments.push(tsSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining text is status text
|
||||
const text = this.payload.substring(offset);
|
||||
if (!text) return { payload: null };
|
||||
|
||||
// Detect trailing Maidenhead locator (4 or 6 chars) at end of text separated by space
|
||||
let maidenhead: string | undefined;
|
||||
const mhMatch = text.match(/\s([A-Ra-r]{2}\d{2}(?:[A-Ra-r]{2})?)$/);
|
||||
let statusText = text;
|
||||
if (mhMatch) {
|
||||
maidenhead = mhMatch[1].toUpperCase();
|
||||
statusText = text.slice(0, mhMatch.index).trimEnd();
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
type: 'status',
|
||||
timestamp: undefined,
|
||||
text: statusText,
|
||||
};
|
||||
|
||||
// If timestamp was parsed, attach it
|
||||
if (segments.length > 0) {
|
||||
// The first segment may be timestamp; parseTimestamp returns the Timestamp object
|
||||
// Re-parse to obtain timestamp object (cheap) - alternate would be to capture earlier
|
||||
const timeSegment = segments.find(s => s.name === 'timestamp');
|
||||
if (timeSegment) {
|
||||
const tsStr = new TextDecoder().decode(timeSegment.data);
|
||||
const { timestamp } = this.parseTimestamp(tsStr, false, 0);
|
||||
if (timestamp) payload.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
if (maidenhead) payload.maidenhead = maidenhead;
|
||||
|
||||
if (withStructure) {
|
||||
segments.push({
|
||||
name: 'status',
|
||||
data: new TextEncoder().encode(text),
|
||||
fields: [
|
||||
{ type: FieldType.STRING, name: 'text', size: text.length },
|
||||
],
|
||||
});
|
||||
return { payload, segment: segments };
|
||||
}
|
||||
|
||||
return { payload };
|
||||
}
|
||||
|
||||
private decodeQuery(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeQuery(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement query decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeTelemetry(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeTelemetry(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement telemetry decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeWeather(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeWeather(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement weather decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeRawGPS(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeRawGPS(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement raw GPS decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeCapabilities(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeCapabilities(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement capabilities decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeUserDefined(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeUserDefined(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement user-defined decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
private decodeThirdParty(_withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
private decodeThirdParty(withStructure: boolean = false): { payload: Payload | null; segment?: PacketSegment[] } {
|
||||
// TODO: Implement third-party decoding with section emission
|
||||
return { payload: null };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user