Files
aprs.ts/src/payload.weather.ts

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;