import { FieldType, type Segment } from "@hamradio/packet"; import { DataType, type IPosition, type Payload, type WeatherPayload } from "./frame.types"; import { isCompressedPosition, parseCompressedPosition, parseUncompressedPosition } from "./payload.position"; import Timestamp from "./timestamp"; export const decodeWeatherPayload = ( raw: string, withStructure: boolean = false ): { payload: Payload | null; segment?: Segment[]; } => { try { if (raw.length < 2) return { payload: null }; let offset = 1; // skip '_' data type const segments: Segment[] = withStructure ? [] : []; // Try optional timestamp (7 chars) let timestamp; if (raw.length >= offset + 7) { const timeStr = raw.substring(offset, offset + 7); const parsed = Timestamp.fromString(timeStr, withStructure); timestamp = parsed.timestamp; if (parsed.segment) { segments.push(parsed.segment); } if (timestamp) offset += 7; } // Try optional position following timestamp let position: IPosition | undefined; let consumed = 0; const tail = raw.substring(offset); if (tail.length > 0) { // If the tail starts with a wind token like DDD/SSS, treat it as weather data // and do not attempt to parse it as a position (avoids mis-detecting wind // values as compressed position fields). if (/^\s*\d{3}\/\d{1,3}/.test(tail)) { // no position present; leave consumed = 0 } else if (isCompressedPosition(tail)) { const parsed = parseCompressedPosition(tail, withStructure); if (parsed.position) { position = { latitude: parsed.position.latitude, longitude: parsed.position.longitude, symbol: parsed.position.symbol, altitude: parsed.position.altitude }; if (parsed.segment) segments.push(parsed.segment); consumed = 13; } } else { const parsed = parseUncompressedPosition(tail, withStructure); if (parsed.position) { position = { latitude: parsed.position.latitude, longitude: parsed.position.longitude, symbol: parsed.position.symbol, ambiguity: parsed.position.ambiguity }; if (parsed.segment) segments.push(parsed.segment); consumed = 19; } } } offset += consumed; const rest = raw.substring(offset).trim(); const payload: WeatherPayload = { type: DataType.WeatherReportNoPosition }; if (timestamp) payload.timestamp = timestamp; if (position) payload.position = position; if (rest && rest.length > 0) { // Parse common tokens // Wind: DDD/SSS [gGGG] const windMatch = rest.match(/(\d{3})\/(\d{1,3})(?:g(\d{1,3}))?/); if (windMatch) { payload.windDirection = parseInt(windMatch[1], 10); payload.windSpeed = parseInt(windMatch[2], 10); if (windMatch[3]) payload.windGust = parseInt(windMatch[3], 10); } // Temperature: tNNN (F) const tempMatch = rest.match(/t(-?\d{1,3})/i); if (tempMatch) payload.temperature = parseInt(tempMatch[1], 10); // Rain: rNNN (last hour), pNNN (24h), PNNN (since midnight) - values are hundredths of inch const rMatch = rest.match(/r(\d{3})/); if (rMatch) payload.rainLastHour = parseInt(rMatch[1], 10); const pMatch = rest.match(/p(\d{3})/); if (pMatch) payload.rainLast24Hours = parseInt(pMatch[1], 10); const PMatch = rest.match(/P(\d{3})/); if (PMatch) payload.rainSinceMidnight = parseInt(PMatch[1], 10); // Humidity: hNN const hMatch = rest.match(/h(\d{1,3})/); if (hMatch) payload.humidity = parseInt(hMatch[1], 10); // Pressure: bXXXX or bXXXXX (tenths of millibar) const bMatch = rest.match(/b(\d{4,5})/); if (bMatch) payload.pressure = parseInt(bMatch[1], 10); // Add raw comment payload.comment = rest; if (withStructure) { segments.push({ name: "weather", data: new TextEncoder().encode(rest).buffer, isString: true, fields: [{ type: FieldType.STRING, name: "text", length: rest.length }] }); } } if (withStructure) return { payload, segment: segments }; return { payload }; } catch { return { payload: null }; } }; export default decodeWeatherPayload;