130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
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;
|