150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
import { FieldType, type Segment } from "@hamradio/packet";
|
|
|
|
import { DO_NOT_ARCHIVE_MARKER, DataType, type IPosition, type ItemPayload, type Payload } from "./frame.types";
|
|
import { attachExtras, decodeCommentExtras } from "./payload.extras";
|
|
import { isCompressedPosition, parseCompressedPosition, parseUncompressedPosition } from "./payload.position";
|
|
import Timestamp from "./timestamp";
|
|
|
|
export const decodeItemPayload = (
|
|
raw: string,
|
|
withStructure: boolean = false
|
|
): {
|
|
payload: Payload | null;
|
|
segment?: Segment[];
|
|
} => {
|
|
// 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 (raw.length < 12) return { payload: null }; // minimal: 1 + 3 + 1 + 7
|
|
|
|
let offset = 1; // skip data type identifier ')'
|
|
const segment: Segment[] = withStructure ? [] : [];
|
|
|
|
// Read 9-char name field (pad/truncate as present)
|
|
const rawName = raw.substring(offset, offset + 9);
|
|
const name = rawName.trimEnd();
|
|
if (withStructure) {
|
|
segment.push({
|
|
name: "item name",
|
|
data: new TextEncoder().encode(rawName).buffer,
|
|
isString: true,
|
|
fields: [{ type: FieldType.STRING, name: "name", length: 9 }]
|
|
});
|
|
}
|
|
offset += 9;
|
|
|
|
// State character: '*' = alive, '_' = killed
|
|
const stateChar = raw.charAt(offset);
|
|
if (stateChar !== "*" && stateChar !== "_") {
|
|
return { payload: null };
|
|
}
|
|
const alive = stateChar === "*";
|
|
if (withStructure) {
|
|
segment.push({
|
|
name: "item state",
|
|
data: new TextEncoder().encode(stateChar).buffer,
|
|
isString: true,
|
|
fields: [
|
|
{
|
|
type: FieldType.CHAR,
|
|
name: "State (* alive, _ killed)",
|
|
length: 1
|
|
}
|
|
]
|
|
});
|
|
}
|
|
offset += 1;
|
|
|
|
// Timestamp (7 chars)
|
|
const timeStr = raw.substring(offset, offset + 7);
|
|
const { timestamp, segment: timestampSection } = Timestamp.fromString(timeStr.substring(offset), withStructure);
|
|
if (!timestamp) return { payload: null };
|
|
if (timestampSection) segment.push(timestampSection);
|
|
offset += 7;
|
|
|
|
const isCompressed = isCompressedPosition(raw.substring(offset));
|
|
|
|
// eslint-disable-next-line no-useless-assignment
|
|
let position: IPosition | null = null;
|
|
// eslint-disable-next-line no-useless-assignment
|
|
let consumed = 0;
|
|
|
|
if (isCompressed) {
|
|
const { position: compressed, segment: compressedSection } = parseCompressedPosition(
|
|
raw.substring(offset),
|
|
withStructure
|
|
);
|
|
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 } = parseUncompressedPosition(
|
|
raw.substring(offset),
|
|
withStructure
|
|
);
|
|
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 remainder = raw.substring(offset);
|
|
const doNotArchive = remainder.includes(DO_NOT_ARCHIVE_MARKER);
|
|
let comment = remainder;
|
|
|
|
const extras = decodeCommentExtras(comment, withStructure);
|
|
comment = extras.comment;
|
|
|
|
if (comment) {
|
|
position.comment = comment;
|
|
if (withStructure) {
|
|
segment.push({
|
|
name: "comment",
|
|
data: new TextEncoder().encode(remainder).buffer,
|
|
isString: true,
|
|
fields: extras.fields || []
|
|
});
|
|
}
|
|
} else if (withStructure && extras.fields) {
|
|
// No free-text comment, but extras fields exist: emit comment-only segment
|
|
segment.push({
|
|
name: "comment",
|
|
data: new TextEncoder().encode(remainder).buffer,
|
|
isString: true,
|
|
fields: extras.fields || []
|
|
});
|
|
}
|
|
|
|
const payload: ItemPayload = {
|
|
type: DataType.Item,
|
|
doNotArchive,
|
|
name,
|
|
alive,
|
|
position
|
|
};
|
|
attachExtras(payload, extras);
|
|
|
|
if (withStructure) {
|
|
return { payload, segment };
|
|
}
|
|
|
|
return { payload };
|
|
};
|
|
|
|
export default decodeItemPayload;
|