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;