Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 104 additions & 26 deletions lib/plugins/Label_21_POS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,42 @@ import { DecoderPlugin } from '../DecoderPlugin';
import { DecodeResult, Message, Options } from '../DecoderPluginInterface';
import { ResultFormatter } from '../utils/result_formatter';

// Position Report
/**
* Label 21 — Periodic Position Report (POSN)
*
* Automatic in-flight position report transmitted by the FMS/ACARS unit at
* regular intervals. Carries location, ground track, time, altitude, speed,
* wind, OAT, ETA, and destination.
*
* Wire format (after the `POS` preamble — note that the trailing `N` of
* `POSN` becomes the first character of the body):
*
* N 34.398W 80.393, 85,230818,35019,24306, 92,-54,235606,KBWI
* | | | | | | | | | | |
* | | | | | | | | | | └ Destination ICAO
* | | | | | | | | | └─────── ETA, HHMMSS UTC
* | | | | | | | | └─────────── OAT (°C, may be negative)
* | | | | | | | └─────────────── Wind speed (kt)
* | | | | | | └─────────────────────── Groundspeed × 100
* | | | | | | (raw integer; 24306 → 243.06 kt)
* | | | | | └───────────────────────────── Altitude (ft MSL)
* | | | | └──────────────────────────────────── Time of position, HHMMSS UTC
* | | | └─────────────────────────────────────── Ground track (degrees true)
* | | └─────────────────────────────────────────────── Longitude (deg, sign from hemisphere)
* | └─────────────────────────────────────────────────────── Longitude hemisphere (E/W)
* └───────────────────────────────────────────────────────── Latitude (deg) — leading hemisphere
* char (N/S) supplied by the trailing
* `N` of `POSN`; observed examples are
* all northern.
*
* Variants seen in the wild (longitude width 6 or 7 chars, optional space):
* N 34.398W 80.393, ... (lon < 100)
* N 37.761W121.680, ... (lon ≥ 100, no space)
*
* Per the analyst's "wild-guess → ignore" guidance, latitude is parsed as
* implicitly North when the hemisphere letter is the trailing `N` of `POSN`;
* a leading `S` would be honoured but is undocumented.
*/
export class Label_21_POS extends DecoderPlugin {
name = 'label-21-pos';

Expand All @@ -22,30 +57,72 @@ export class Label_21_POS extends DecoderPlugin {
const content = message.text.substring(3);
const fields = content.split(',');

if (fields.length == 9) {
// POSN 37.550W 76.436, 98,110800,23961,25820, 65,-23,114212,KRDU
if (fields.length === 9) {
// ── Position (field 0) ──
processPosition(decodeResult, fields[0].trim());

// ── Field 1: ground track (degrees true) ──
const trackRaw = fields[1].trim();
if (/^\d{1,3}$/.test(trackRaw)) {
const track = Number(trackRaw);
decodeResult.raw.track = track;
decodeResult.formatted.items.push({
type: 'track',
code: 'TRK',
label: 'Ground Track',
value: `${track}°`,
});
}

// ── Field 2: time of position, HHMMSS ──
ResultFormatter.timestamp(
decodeResult,
DateTimeUtils.convertHHMMSSToTod(fields[2]),
);

// ── Field 3: altitude (ft MSL) ──
ResultFormatter.altitude(decodeResult, Number(fields[3]));

// ── Field 4: groundspeed × 100 (raw) ──
const gsRaw = fields[4].trim();
if (/^\d{3,6}$/.test(gsRaw)) {
const gsKts = Number(gsRaw) / 100;
decodeResult.raw.ground_speed = gsKts;
decodeResult.formatted.items.push({
type: 'ground_speed',
code: 'GS',
label: 'Ground Speed',
value: `${gsKts.toFixed(2)} kt`,
});
}

// ── Field 5: wind speed (kt) ──
const windRaw = fields[5].trim();
if (/^-?\d{1,3}$/.test(windRaw)) {
const wind = Number(windRaw);
decodeResult.raw.wind_speed = wind;
decodeResult.formatted.items.push({
type: 'wind_speed',
code: 'WIND',
label: 'Wind Speed',
value: `${wind} kt`,
});
}

// ── Field 6: OAT (°C) ──
ResultFormatter.temperature(decodeResult, fields[6].replace(/ /g, ''));

// ── Field 7: ETA, HHMMSS ──
ResultFormatter.eta(
decodeResult,
DateTimeUtils.convertHHMMSSToTod(fields[7]),
);
ResultFormatter.arrivalAirport(decodeResult, fields[8]);

ResultFormatter.unknownArr(decodeResult, [
fields[1],
fields[4],
fields[5],
]);
// ── Field 8: destination ICAO ──
ResultFormatter.arrivalAirport(decodeResult, fields[8]);

this.setDecodeLevel(decodeResult, true, 'partial');
this.setDecodeLevel(decodeResult, true, 'full');
} else {
// Unknown!
this.debug(
options,
`Unknown variation. Field count: ${fields.length}, content: ${content}`,
Expand All @@ -55,23 +132,24 @@ export class Label_21_POS extends DecoderPlugin {
return decodeResult;
}
}

/**
* Parse the position field. Accepts both spaced and unspaced forms:
* "N 34.398W 80.393"
* "N 37.761W121.680"
* "S 33.940E151.180" (hypothetical southern/eastern; not observed)
*/
function processPosition(decodeResult: DecodeResult, value: string) {
// N 39.841W 75.790
if (
value.length !== 16 &&
value[0] !== 'N' &&
value[0] !== 'S' &&
value[8] !== 'W' &&
value[8] !== 'E'
) {
return;
}
const latDir = value[0] === 'N' ? 1 : -1;
const lonDir = value[8] === 'E' ? 1 : -1;
const m = value.match(
/^([NS])\s*(\d+(?:\.\d+)?)\s*([EW])\s*(\d+(?:\.\d+)?)$/,
);
if (!m) return;

const latDir = m[1] === 'N' ? 1 : -1;
const lonDir = m[3] === 'E' ? 1 : -1;
const position = {
latitude: latDir * Number(value.substring(1, 7)),
longitude: lonDir * Number(value.substring(9, 15)),
latitude: latDir * Number(m[2]),
longitude: lonDir * Number(m[4]),
};

ResultFormatter.position(decodeResult, position);
}