-
-
Notifications
You must be signed in to change notification settings - Fork 18
Update Label SQ — accept 'B' separator, fix DDMM coordinate encoding #423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,43 @@ | ||
| import { DecoderPlugin } from '../DecoderPlugin'; | ||
| import { DecodeResult, Message, Options } from '../DecoderPluginInterface'; | ||
|
|
||
| /** | ||
| * Label SQ — Ground Station Squitter | ||
| * | ||
| * ACARS label SQ denotes a "squitter" — an ID and uplink test message | ||
| * transmitted at regular intervals from ACARS ground stations. It is a | ||
| * ground-to-air BROADCAST (not a downlink from an aircraft), announcing | ||
| * the ground station's presence, position, and VDL-2 capability to any | ||
| * aircraft in range. | ||
| * | ||
| * Extended squitter (version 02) format: | ||
| * | ||
| * 0 2 X A YVR CYVR 1 4911 N 12310 W V 136975 /ARINC | ||
| * | | | | | | | | | | | | | | | ||
| * | | | | | | | | | | | | | └ Provider suffix: /ARINC or empty (SITA) | ||
| * | | | | | | | | | | | | └──────── VDL-2 frequency (kHz), 136975 → 136.975 MHz | ||
| * | | | | | | | | | | | └────────── Separator flag (often 'V' for VDL, but 'B' | ||
| * | | | | | | | | | | | and other letters have been observed; the | ||
| * | | | | | | | | | | | specific meaning is not documented) | ||
| * | | | | | | | | | | └──────────── Longitude hemisphere (E/W) | ||
| * | | | | | | | | | └──────────────── Longitude (DDDMM) — deg + 2 digits min | ||
| * | | | | | | | | └────────────────── Latitude hemisphere (N/S) | ||
| * | | | | | | | └─────────────────────── Latitude (DDMM) — deg + 2 digits min | ||
| * | | | | | | └───────────────────────── Ground station number (1-9) | ||
| * | | | | | └────────────────────────────── ICAO airport code of the station (4 chars) | ||
| * | | | | └────────────────────────────────── IATA airport code of the station (3 chars) | ||
| * | | | └──────────────────────────────────── Provider ID: A = ARINC (RC), S = SITA | ||
| * | | └────────────────────────────────────── Category: X = ground-to-air uplink broadcast | ||
| * | └──────────────────────────────────────── Version: 2 = extended squitter with position + VDL-2 | ||
| * └────────────────────────────────────────── Version digit 1 (always '0') | ||
| * | ||
| * Coordinate encoding: lat is 4 digits DDMM, lng is 5 digits DDDMM. | ||
| * Example: 5109 N 00012 W → 51°09'N, 000°12'W → London Gatwick (EGKK). | ||
| * | ||
| * Real-world examples observed: | ||
| * 0EYVRCYVR14911N12310WV136975/ARINC ← Vancouver, V separator | ||
| * 02XALGWEGKK15109N00012WB136975/ARINC ← Gatwick, B separator | ||
| */ | ||
| export class Label_SQ extends DecoderPlugin { | ||
| name = 'label-sq'; | ||
|
|
||
|
|
@@ -11,42 +48,76 @@ export class Label_SQ extends DecoderPlugin { | |
| } | ||
|
|
||
| decode(message: Message, options: Options = {}): DecodeResult { | ||
| const decodeResult = this.initResult(message, 'Ground Station Squitter'); | ||
| const decodeResult = this.initResult( | ||
| message, | ||
| 'Ground Station Squitter (ID / Uplink Test — ground-to-air broadcast)' | ||
| ); | ||
|
|
||
| decodeResult.raw.preamble = message.text.substring(0, 4); | ||
| decodeResult.raw.version = Number(message.text.substring(1, 2)); | ||
| decodeResult.raw.network = message.text.substring(3, 4); | ||
| // Category char (typically 'X' for uplink broadcast) | ||
| decodeResult.raw.category = message.text.substring(2, 3); | ||
|
|
||
| if (decodeResult.raw.version === 2) { | ||
| // Lat is 4 digits (DDMM), lng is 5 digits (DDDMM). Separator before | ||
| // frequency is typically 'V' but 'B' (and possibly others) have been | ||
| // observed; capture as a raw flag rather than constraining to 'V'. | ||
| const regex = | ||
| /0(\d)X(?<org>\w)(?<iata>\w\w\w)(?<icao>\w\w\w\w)(?<station>\d)(?<lat>\d+)(?<latd>[NS])(?<lng>\d+)(?<lngd>[EW])V(?<vfreq>\d+)\/.*/; | ||
| /0(\d)(?<cat>[A-Z])(?<org>[A-Z])(?<iata>\w{3})(?<icao>\w{4})(?<station>\d)(?<lat>\d{4})(?<latd>[NS])(?<lng>\d{5})(?<lngd>[EW])(?<sep>[A-Z])(?<vfreq>\d+)(?:\/(?<suffix>\w*))?/; | ||
| const result = message.text.match(regex); | ||
|
|
||
| if (result?.groups && result.length >= 8) { | ||
| decodeResult.raw.category = result.groups.cat; | ||
|
Comment on lines
70
to
+71
|
||
| decodeResult.raw.network = result.groups.org; | ||
|
|
||
| // DDMM → decimal degrees (deg + min/60), signed by hemisphere | ||
| const latRaw = result.groups.lat; | ||
| const lngRaw = result.groups.lng; | ||
| const latDeg = Math.floor(Number(latRaw) / 100); | ||
| const latMin = Number(latRaw) % 100; | ||
| const lngDeg = Math.floor(Number(lngRaw) / 100); | ||
| const lngMin = Number(lngRaw) % 100; | ||
| const latitude = | ||
| (latDeg + latMin / 60) * (result.groups.latd === 'S' ? -1 : 1); | ||
| const longitude = | ||
| (lngDeg + lngMin / 60) * (result.groups.lngd === 'W' ? -1 : 1); | ||
|
Comment on lines
+63
to
+84
|
||
|
|
||
| decodeResult.raw.groundStation = { | ||
| number: result.groups.station, | ||
| iataCode: result.groups.iata, | ||
| icaoCode: result.groups.icao, | ||
| coordinates: { | ||
| latitude: | ||
| (Number(result.groups.lat) / 100) * | ||
| (result.groups.latd === 'S' ? -1 : 1), | ||
| longitude: | ||
| (Number(result.groups.lng) / 100) * | ||
| (result.groups.lngd === 'W' ? -1 : 1), | ||
| latitude: Math.round(latitude * 1e6) / 1e6, | ||
| longitude: Math.round(longitude * 1e6) / 1e6, | ||
| }, | ||
| }; | ||
| decodeResult.raw.vdlFrequency = Number(result.groups.vfreq) / 1000.0; | ||
| decodeResult.raw.separator = result.groups.sep; | ||
| if (result.groups.suffix) { | ||
| decodeResult.raw.providerSuffix = result.groups.suffix; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var formattedNetwork = 'Unknown'; | ||
| if (decodeResult.raw.network == 'A') { | ||
| if (decodeResult.raw.network === 'A') { | ||
| formattedNetwork = 'ARINC'; | ||
| } else if (decodeResult.raw.network == 'S') { | ||
| } else if (decodeResult.raw.network === 'S') { | ||
| formattedNetwork = 'SITA'; | ||
| } | ||
| var formattedCategory = String(decodeResult.raw.category || ''); | ||
| if (formattedCategory === 'X') { | ||
|
Comment on lines
103
to
+110
|
||
| formattedCategory = 'X (ground-to-air uplink broadcast)'; | ||
| } | ||
|
|
||
| decodeResult.formatted.items = [ | ||
| { | ||
| type: 'message_type', | ||
| code: 'MSGTYP', | ||
| label: 'Message Type', | ||
| value: 'Squitter (ID / Uplink Test — ground-to-air broadcast)', | ||
| }, | ||
| { | ||
| type: 'network', | ||
| code: 'NETT', | ||
|
|
@@ -59,6 +130,12 @@ export class Label_SQ extends DecoderPlugin { | |
| label: 'Version', | ||
| value: String(decodeResult.raw.version), | ||
| }, | ||
| { | ||
| type: 'category', | ||
| code: 'CAT', | ||
| label: 'Category', | ||
| value: formattedCategory, | ||
| }, | ||
| ]; | ||
|
|
||
| if (decodeResult.raw.groundStation) { | ||
|
|
@@ -115,6 +192,25 @@ export class Label_SQ extends DecoderPlugin { | |
| value: `${decodeResult.raw.vdlFrequency} MHz`, | ||
| }); | ||
| } | ||
|
|
||
| if (decodeResult.raw.separator) { | ||
| decodeResult.formatted.items.push({ | ||
| type: 'separator', | ||
| code: 'SEP', | ||
| label: 'Separator Flag', | ||
| value: String(decodeResult.raw.separator), | ||
| }); | ||
| } | ||
|
|
||
| if (decodeResult.raw.providerSuffix) { | ||
| decodeResult.formatted.items.push({ | ||
| type: 'provider_suffix', | ||
| code: 'PROVSFX', | ||
| label: 'Provider Suffix', | ||
| value: `/${decodeResult.raw.providerSuffix}`, | ||
| }); | ||
| } | ||
|
|
||
| this.setDecodeLevel(decodeResult, true, 'full'); | ||
|
|
||
| return decodeResult; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex uses
\w{3}/\w{4}for IATA/ICAO, which also matches digits and underscores. Since these fields are documented here as airport codes and other captured fields are constrained to[A-Z], consider tightening these to[A-Z]{3}and[A-Z]{4}to avoid accepting invalid station identifiers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maintainer reviewer agree — Should Fix. Tightening
\\wto[A-Z]for IATA/ICAO is the right call:\\waccepts[A-Za-z0-9_], none of which (except A-Z) are valid in airport codes. The Copilot suggested edit is good as written — please apply.