@squawk/mode-s 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +243 -0
  2. package/dist/acas.d.ts +14 -0
  3. package/dist/acas.d.ts.map +1 -0
  4. package/dist/acas.js +90 -0
  5. package/dist/altitude.d.ts +34 -0
  6. package/dist/altitude.d.ts.map +1 -0
  7. package/dist/altitude.js +99 -0
  8. package/dist/bits.d.ts +41 -0
  9. package/dist/bits.d.ts.map +1 -0
  10. package/dist/bits.js +55 -0
  11. package/dist/comm-b.d.ts +37 -0
  12. package/dist/comm-b.d.ts.map +1 -0
  13. package/dist/comm-b.js +245 -0
  14. package/dist/cpr.d.ts +71 -0
  15. package/dist/cpr.d.ts.map +1 -0
  16. package/dist/cpr.js +220 -0
  17. package/dist/emergency-status.d.ts +10 -0
  18. package/dist/emergency-status.d.ts.map +1 -0
  19. package/dist/emergency-status.js +21 -0
  20. package/dist/frame.d.ts +42 -0
  21. package/dist/frame.d.ts.map +1 -0
  22. package/dist/frame.js +68 -0
  23. package/dist/identification.d.ts +10 -0
  24. package/dist/identification.d.ts.map +1 -0
  25. package/dist/identification.js +64 -0
  26. package/dist/identity.d.ts +11 -0
  27. package/dist/identity.d.ts.map +1 -0
  28. package/dist/identity.js +20 -0
  29. package/dist/index.d.ts +32 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +21 -0
  32. package/dist/message.d.ts +40 -0
  33. package/dist/message.d.ts.map +1 -0
  34. package/dist/message.js +293 -0
  35. package/dist/mode-ac.d.ts +17 -0
  36. package/dist/mode-ac.d.ts.map +1 -0
  37. package/dist/mode-ac.js +86 -0
  38. package/dist/operational-status.d.ts +9 -0
  39. package/dist/operational-status.d.ts.map +1 -0
  40. package/dist/operational-status.js +23 -0
  41. package/dist/surface-movement.d.ts +9 -0
  42. package/dist/surface-movement.d.ts.map +1 -0
  43. package/dist/surface-movement.js +37 -0
  44. package/dist/target-state-status.d.ts +9 -0
  45. package/dist/target-state-status.d.ts.map +1 -0
  46. package/dist/target-state-status.js +34 -0
  47. package/dist/test-utils.d.ts +7 -0
  48. package/dist/test-utils.d.ts.map +1 -0
  49. package/dist/test-utils.js +17 -0
  50. package/dist/types/acas.d.ts +82 -0
  51. package/dist/types/acas.d.ts.map +1 -0
  52. package/dist/types/acas.js +1 -0
  53. package/dist/types/comm-b.d.ts +69 -0
  54. package/dist/types/comm-b.d.ts.map +1 -0
  55. package/dist/types/comm-b.js +1 -0
  56. package/dist/types/cpr.d.ts +25 -0
  57. package/dist/types/cpr.d.ts.map +1 -0
  58. package/dist/types/cpr.js +1 -0
  59. package/dist/types/emergency-status.d.ts +6 -0
  60. package/dist/types/emergency-status.d.ts.map +1 -0
  61. package/dist/types/emergency-status.js +1 -0
  62. package/dist/types/frame.d.ts +32 -0
  63. package/dist/types/frame.d.ts.map +1 -0
  64. package/dist/types/frame.js +1 -0
  65. package/dist/types/identification.d.ts +9 -0
  66. package/dist/types/identification.d.ts.map +1 -0
  67. package/dist/types/identification.js +1 -0
  68. package/dist/types/index.d.ts +12 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +11 -0
  71. package/dist/types/message.d.ts +235 -0
  72. package/dist/types/message.d.ts.map +1 -0
  73. package/dist/types/message.js +1 -0
  74. package/dist/types/mode-ac.d.ts +16 -0
  75. package/dist/types/mode-ac.d.ts.map +1 -0
  76. package/dist/types/mode-ac.js +1 -0
  77. package/dist/types/operational-status.d.ts +28 -0
  78. package/dist/types/operational-status.d.ts.map +1 -0
  79. package/dist/types/operational-status.js +1 -0
  80. package/dist/types/target-state-status.d.ts +34 -0
  81. package/dist/types/target-state-status.d.ts.map +1 -0
  82. package/dist/types/target-state-status.js +1 -0
  83. package/dist/types/velocity.d.ts +35 -0
  84. package/dist/types/velocity.d.ts.map +1 -0
  85. package/dist/types/velocity.js +1 -0
  86. package/dist/velocity.d.ts +9 -0
  87. package/dist/velocity.d.ts.map +1 -0
  88. package/dist/velocity.js +73 -0
  89. package/package.json +61 -0
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ <h1><img src="../../../assets/squawk-logo.svg" alt="squawk logo" width="48" height="48" style="vertical-align: middle">&nbsp; @squawk/mode-s</h1>
2
+
3
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../LICENSE.md) [![npm](https://img.shields.io/npm/v/@squawk/mode-s)](https://www.npmjs.com/package/@squawk/mode-s) ![TypeScript](https://img.shields.io/badge/TypeScript-blue?logo=typescript&logoColor=white)
4
+
5
+ Decodes raw Mode-S/ADS-B messages: downlink format and CRC extraction, CPR
6
+ position, airborne velocity, aircraft identification, altitude (both the
7
+ ADS-B position-message field and legacy Gillham-coded surveillance replies),
8
+ squawk identity, emergency status, ACAS/TCAS Resolution Advisories, target
9
+ state and status, aircraft operational status, and Enhanced Surveillance
10
+ Comm-B registers (selected vertical intention, track and turn, heading and
11
+ speed). Transport-agnostic - it operates on already-framed message bytes and
12
+ has no opinion about where they came from (a live Beast feed, a logged
13
+ capture, or any other source). For a Beast binary parser and live TCP client
14
+ built on top of this package, see [`@squawk/beast`](../beast).
15
+
16
+ Part of the [@squawk](https://www.npmjs.com/org/squawk) aviation library suite. See all packages on npm.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @squawk/mode-s
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Decoding a whole message
27
+
28
+ `decodeModeSMessage` is the main entry point - it reads the downlink format,
29
+ validates the CRC, and routes to the right per-type decoder, returning a
30
+ single discriminated result.
31
+
32
+ ```typescript
33
+ import { decodeModeSMessage } from '@squawk/mode-s';
34
+
35
+ const decoded = decodeModeSMessage(rawMessageBytes); // 7 or 14 raw bytes
36
+ if (decoded?.kind === 'extendedSquitterPosition') {
37
+ console.log(decoded.icaoHex, decoded.latCpr, decoded.lonCpr, decoded.altitudeFt);
38
+ }
39
+ ```
40
+
41
+ `bytes`' length must match what its downlink format implies (7 bytes for a
42
+ short message, 14 for long) or the message is rejected outright - a length
43
+ mismatch means the buffer is truncated or corrupted, not a genuine message
44
+ of that format.
45
+
46
+ DF17/18 (extended squitter) messages are only decoded when their CRC checks
47
+ out exactly - a corrupted squitter reports as undecodable rather than
48
+ returning plausible-looking wrong data. DF18 is further gated on its control
49
+ field: CF=0/1/2/5/6 share DF17's type-code-coded ME layout and decode the
50
+ same way; CF=3 (TIS-B coarse format) uses a different field layout this
51
+ package does not decode, and CF=4/7 carry no per-aircraft state. Every
52
+ decoded DF17/18 message carries `messageSource`, so a consumer can tell a
53
+ real, direct ICAO address from an anonymous or ground-derived one rather
54
+ than losing that distinction after the gate:
55
+
56
+ ```typescript
57
+ const decoded = decodeModeSMessage(rawMessageBytes);
58
+ if (decoded?.kind === 'extendedSquitterPosition' && decoded.messageSource !== 'icaoDirect') {
59
+ // decoded.icaoHex may not be a registered ICAO address (anonymousDirect/anonymousTisb),
60
+ // or the position may be ground-derived rather than heard directly (icaoTisb/adsr)
61
+ }
62
+ ```
63
+
64
+ DF0/4/5/20/21 (Mode-S surveillance replies) are targeted responses whose CRC
65
+ is XORed with the responding aircraft's ICAO address rather than being a
66
+ plain checksum, so their `candidateIcaoHex` needs cross-checking against an
67
+ address already known from squitter traffic before it can be trusted - this
68
+ package doesn't perform that cross-check itself. DF20/21 (`'commBAltitudeReply'`/`'commBIdentityReply'`)
69
+ carry the same altitude/squawk payload as DF4/5 plus a 56-bit MB field,
70
+ decoded into `commBRegisters` - see
71
+ [Enhanced Surveillance Comm-B registers](#enhanced-surveillance-comm-b-registers).
72
+
73
+ ### Resolving CPR position
74
+
75
+ Airborne and surface position messages carry raw CPR-encoded lat/lon
76
+ fields, not a directly usable position - resolving them needs either a
77
+ paired even/odd frame or a nearby reference position, and pairing state
78
+ (tracking the most recent even and odd frame per aircraft) is the caller's
79
+ responsibility, not this package's:
80
+
81
+ ```typescript
82
+ import { decodeAirborneCprPair, decodeAirborneCprWithReference } from '@squawk/mode-s';
83
+
84
+ // From a paired even + odd frame (global decode, no reference needed)
85
+ const position = decodeAirborneCprPair(evenFrame, oddFrame, 'even');
86
+
87
+ // From a single frame plus a known-nearby reference (e.g. the aircraft's
88
+ // last known position, or the receiver's own location for a new aircraft)
89
+ const position2 = decodeAirborneCprWithReference('even', frame, referencePosition);
90
+ ```
91
+
92
+ `decodeSurfaceCprPair` / `decodeSurfaceCprWithReference` are the equivalents
93
+ for on-ground traffic (type codes 5-8), which additionally need a reference
94
+ position to resolve the correct hemisphere and longitude quadrant.
95
+
96
+ An airborne position message with type code 0 signals that no position fix
97
+ is currently available - `latCpr`/`lonCpr` are undefined in that case even
98
+ though `altitudeFt` may still be populated.
99
+
100
+ ### ACAS / TCAS Resolution Advisories
101
+
102
+ An active Resolution Advisory reaches `decodeModeSMessage` two ways: DF16
103
+ (a targeted air-air surveillance reply, replacing DF0 while an RA is active)
104
+ and a DF17/18 type-code-28 subtype-2 message (the same report broadcast over
105
+ ADS-B so aircraft without interrogation capability can still see it).
106
+
107
+ ```typescript
108
+ const decoded = decodeModeSMessage(rawMessageBytes);
109
+ if (decoded?.kind === 'longAirAirSurveillanceReply' && decoded.resolutionAdvisory?.active) {
110
+ console.log(decoded.resolutionAdvisory.advisoryType); // e.g. 'climb', 'crossingDescend', 'increaseClimb'
111
+ }
112
+ ```
113
+
114
+ `advisoryType` names the RA per RTCA DO-185B Table 2-16 (`climb`, `descend`,
115
+ `crossingClimb`, `crossingDescend`, `increaseClimb`, `increaseDescent`,
116
+ `reduceClimb`, `reduceDescent`, `doNotClimb`, `doNotDescend`,
117
+ `reversalToClimb`, `reversalToDescend`), derived from the report's
118
+ corrective/sense/rate/crossing/reversal flags - all of which are also
119
+ exposed individually on `AcasResolutionAdvisoryReport`, along with the
120
+ Resolution Advisory Complement flags (`doNotPassBelow`/`doNotPassAbove`;
121
+ `doNotTurnLeft`/`doNotTurnRight` are decoded for completeness but TCAS II
122
+ issues vertical RAs only, so they are expected to always be false). `advisoryType`
123
+ is undefined when no RA is currently active, or for the one flag combination
124
+ DO-185B leaves undefined (positive + preventive).
125
+
126
+ `threat` identifies what the RA is responding to, discriminated by
127
+ `threat.threatType`:
128
+
129
+ ```typescript
130
+ if (decoded.resolutionAdvisory?.threat.threatType === 'icaoAddress') {
131
+ console.log(decoded.resolutionAdvisory.threat.threatIcaoHex);
132
+ }
133
+ ```
134
+
135
+ `'none'` carries no further fields; `'icaoAddress'` carries `threatIcaoHex`;
136
+ `'altitudeRangeBearing'` carries `threatAltitudeFt`/`threatRangeNm`/`threatBearingDeg`
137
+ (each independently undefined if that specific field is unavailable).
138
+
139
+ ### Target state and status
140
+
141
+ A type-code-29 ADS-B message reports what the aircraft's flight management
142
+ system is currently targeting - selected altitude, selected heading, and
143
+ which autopilot modes are engaged - not the aircraft's actual state:
144
+
145
+ ```typescript
146
+ const decoded = decodeModeSMessage(rawMessageBytes);
147
+ if (decoded?.kind === 'extendedSquitterTargetStateAndStatus') {
148
+ console.log(
149
+ decoded.targetStateAndStatus.selectedAltitudeFt,
150
+ decoded.targetStateAndStatus.selectedHeadingDeg,
151
+ );
152
+ }
153
+ ```
154
+
155
+ Each field is independently undefined when its source data isn't available -
156
+ `selectedAltitudeSource` further distinguishes an MCP/FCU-selected altitude
157
+ from an FMS-selected one. `autopilotEngaged`/`vnavModeActive`/`altitudeHoldModeActive`/`approachModeActive`/`lnavModeActive`
158
+ are reported together (all defined or all undefined) since they share a
159
+ single status bit; `tcasOperational` has its own independent status.
160
+
161
+ ### Aircraft operational status
162
+
163
+ A type-code-31 ADS-B message reports the transmitting aircraft's ADS-B
164
+ version, surveillance integrity/accuracy figures, and heading reference -
165
+ metadata about the quality of the aircraft's other broadcasts rather than
166
+ its position or state:
167
+
168
+ ```typescript
169
+ const decoded = decodeModeSMessage(rawMessageBytes);
170
+ if (decoded?.kind === 'extendedSquitterOperationalStatus') {
171
+ console.log(
172
+ decoded.operationalStatus.adsbVersion,
173
+ decoded.operationalStatus.navAccuracyCategoryPosition,
174
+ );
175
+ }
176
+ ```
177
+
178
+ `capabilityClassCode`/`operationalModeCode` are exposed as their raw 16-bit
179
+ values rather than individually decoded sub-fields. `nicBaro` is only
180
+ populated for an airborne report on ADS-B version 1+; `silSupplementPerHour`
181
+ is only populated on version 2, where SIL changes from "per sample" to a
182
+ choice between per-sample and per-hour.
183
+
184
+ ### Enhanced Surveillance Comm-B registers
185
+
186
+ DF20/21's 56-bit MB field can carry any of several "Enhanced Surveillance"
187
+ Comm-B registers (BDS 4,0 selected vertical intention, BDS 5,0 track and
188
+ turn report, BDS 6,0 heading and speed report), but unlike a DF17/18 ME
189
+ field's type code, nothing in the message itself declares which one. This
190
+ package validates the MB field's bytes against each register's expected
191
+ structure (status-bit consistency, reserved bits, physically-plausible
192
+ ranges) and returns every register that plausibly matches - usually exactly
193
+ one, occasionally none, and occasionally more than one when the bytes are
194
+ genuinely ambiguous (BDS 5,0 and 6,0 are the pair most likely to overlap):
195
+
196
+ ```typescript
197
+ const decoded = decodeModeSMessage(rawMessageBytes);
198
+ if (decoded?.kind === 'commBAltitudeReply') {
199
+ for (const register of decoded.commBRegisters) {
200
+ if (register.bdsCode === '5,0') {
201
+ console.log(register.rollAngleDeg, register.trueTrackDeg, register.groundSpeedKt);
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ `inferCommBRegisters(mb)` is also exported directly for callers working with
208
+ a raw MB field outside of `decodeModeSMessage`'s DF20/21 dispatch.
209
+
210
+ ### Mode A/C
211
+
212
+ Mode A/C predates Mode-S and carries no ICAO address, so it has no natural
213
+ key in an ICAO-hex-indexed tracking model - it's decoded here, but
214
+ deliberately not part of `decodeModeSMessage`'s dispatch:
215
+
216
+ ```typescript
217
+ import { decodeModeAc } from '@squawk/mode-s';
218
+
219
+ const reply = decodeModeAc(twoRawBytes);
220
+ console.log(reply.squawk, reply.identActive, reply.altitudeFt);
221
+ ```
222
+
223
+ ## API
224
+
225
+ - `decodeModeSMessage(bytes)` - decodes a raw 7 or 14 byte Mode-S message into a discriminated `DecodedModeSMessage`.
226
+ - `decodeModeAc(bytes)` - decodes a raw 2-byte Mode A/C reply.
227
+ - `parseModeSFrame(bytes)`, `extractDownlinkFormat(bytes)`, `computeCrc24(bytes)` - lower-level envelope parsing, used internally by `decodeModeSMessage`.
228
+ - `decodeAirborneCprPair`, `decodeAirborneCprWithReference`, `decodeSurfaceCprPair`, `decodeSurfaceCprWithReference`, `cprNumLongitudeZones` - CPR position resolution.
229
+ - `decodeAirborneVelocity(me)` - airborne velocity/heading from a type-19 ME field.
230
+ - `decodeIdentification(me)` - callsign and category from a type 1-4 ME field.
231
+ - `decodeIdentityCode(idField)` - 4-digit octal squawk from a 13-bit identity field (shared by DF5/21 and ADS-B emergency status).
232
+ - `decodeAltitudeCode(altitudeCode)`, `decodeAdsbPositionAltitude(field)`, `decodeAdsbGnssAltitude(field)` - altitude decoding.
233
+ - `decodeSurfaceMovement(field)` - ground speed from a surface position message's movement field.
234
+ - `decodeEmergencyState(rawState)` - emergency/priority state from an ADS-B aircraft status message.
235
+ - `decodeAcasResolutionAdvisory(payload)` - ACAS/TCAS Resolution Advisory report from a DF16 MV field or a type-code-28 subtype-2 ME field.
236
+ - `decodeTargetStateAndStatus(me)` - target state and status from a type-29 ME field.
237
+ - `decodeAircraftOperationalStatus(me)` - operational status from a type-31 ME field.
238
+ - `inferCommBRegisters(mb)` - every Enhanced Surveillance Comm-B register (BDS 4,0/5,0/6,0) a DF20/21 MB field plausibly holds.
239
+ - `decodeSelectedVerticalIntention(mb)`, `decodeTrackAndTurnReport(mb)`, `decodeHeadingAndSpeedReport(mb)` - decode a single Comm-B register directly, given its BDS code is already known.
240
+
241
+ Every decoder that can fail to produce a result returns `undefined` rather
242
+ than throwing - a message that doesn't decode cleanly (unsupported type,
243
+ failed CRC, reserved value) is an expected outcome, not an exceptional one.
package/dist/acas.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { AcasResolutionAdvisoryReport } from './types/index.js';
2
+ /**
3
+ * Decodes an ACAS/TCAS Resolution Advisory report (BDS 3,0 content), shared
4
+ * by DF16's MV field and a DF17/18 type-code-28 subtype-2 ME field - both
5
+ * carry the same 48 bits of content starting at bit 8 of the 7-byte buffer
6
+ * (DF16's MV reserves its first byte for a register-identifier convention;
7
+ * TC28/ST2's ME uses that same first byte for its type-code-and-subtype
8
+ * header instead - either way, content starts at bit 8).
9
+ *
10
+ * @param payload - The 7-byte field: DF16's MV, or a type-code-28 subtype-2 ME.
11
+ * @returns The decoded report, or undefined if the Threat Type Indicator is the reserved value (not a legitimate report).
12
+ */
13
+ export declare function decodeAcasResolutionAdvisory(payload: Uint8Array): AcasResolutionAdvisoryReport | undefined;
14
+ //# sourceMappingURL=acas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acas.d.ts","sourceRoot":"","sources":["../src/acas.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,4BAA4B,EAG7B,MAAM,kBAAkB,CAAC;AA4D1B;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,UAAU,GAClB,4BAA4B,GAAG,SAAS,CAuC1C"}
package/dist/acas.js ADDED
@@ -0,0 +1,90 @@
1
+ import { decodeAltitudeCode } from './altitude.js';
2
+ import { extractBits, formatHexAddress } from './bits.js';
3
+ /** Reserved Threat Type Indicator value - not a legitimate report. */
4
+ const TTI_RESERVED = 3;
5
+ /** Decodes the threat-identity data (bits 28-55) per the Threat Type Indicator, `threatTypeIndicator`. */
6
+ function decodeThreat(payload, threatTypeIndicator) {
7
+ if (threatTypeIndicator === 1) {
8
+ return {
9
+ threatType: 'icaoAddress',
10
+ threatIcaoHex: formatHexAddress(extractBits(payload, 30, 24)),
11
+ };
12
+ }
13
+ if (threatTypeIndicator === 2) {
14
+ const rangeRaw = extractBits(payload, 43, 7);
15
+ const bearingRaw = extractBits(payload, 50, 6);
16
+ return {
17
+ threatType: 'altitudeRangeBearing',
18
+ threatAltitudeFt: decodeAltitudeCode(extractBits(payload, 30, 13)),
19
+ threatRangeNm: rangeRaw > 0 ? (rangeRaw - 1) / 10 : undefined,
20
+ threatBearingDeg: bearingRaw > 0 ? 6 * (bearingRaw - 1) + 3 : undefined,
21
+ };
22
+ }
23
+ return { threatType: 'none' };
24
+ }
25
+ function decodeAdvisoryType(active, corrective, downwardSense, increasedRate, senseReversal, altitudeCrossing, positive) {
26
+ if (!active) {
27
+ return undefined;
28
+ }
29
+ if (!corrective) {
30
+ // Preventive: DO-185B defines no positive-preventive RA type.
31
+ if (positive) {
32
+ return undefined;
33
+ }
34
+ return downwardSense ? 'doNotClimb' : 'doNotDescend';
35
+ }
36
+ if (senseReversal) {
37
+ return downwardSense ? 'reversalToDescend' : 'reversalToClimb';
38
+ }
39
+ if (increasedRate) {
40
+ return downwardSense ? 'increaseDescent' : 'increaseClimb';
41
+ }
42
+ if (!positive) {
43
+ return downwardSense ? 'reduceClimb' : 'reduceDescent';
44
+ }
45
+ if (downwardSense) {
46
+ return altitudeCrossing ? 'crossingDescend' : 'descend';
47
+ }
48
+ return altitudeCrossing ? 'crossingClimb' : 'climb';
49
+ }
50
+ /**
51
+ * Decodes an ACAS/TCAS Resolution Advisory report (BDS 3,0 content), shared
52
+ * by DF16's MV field and a DF17/18 type-code-28 subtype-2 ME field - both
53
+ * carry the same 48 bits of content starting at bit 8 of the 7-byte buffer
54
+ * (DF16's MV reserves its first byte for a register-identifier convention;
55
+ * TC28/ST2's ME uses that same first byte for its type-code-and-subtype
56
+ * header instead - either way, content starts at bit 8).
57
+ *
58
+ * @param payload - The 7-byte field: DF16's MV, or a type-code-28 subtype-2 ME.
59
+ * @returns The decoded report, or undefined if the Threat Type Indicator is the reserved value (not a legitimate report).
60
+ */
61
+ export function decodeAcasResolutionAdvisory(payload) {
62
+ const threatTypeIndicator = extractBits(payload, 28, 2);
63
+ if (threatTypeIndicator === TTI_RESERVED) {
64
+ return undefined;
65
+ }
66
+ const active = extractBits(payload, 8, 1) === 1;
67
+ const corrective = extractBits(payload, 9, 1) === 1;
68
+ const downwardSense = extractBits(payload, 10, 1) === 1;
69
+ const increasedRate = extractBits(payload, 11, 1) === 1;
70
+ const senseReversal = extractBits(payload, 12, 1) === 1;
71
+ const altitudeCrossing = extractBits(payload, 13, 1) === 1;
72
+ const positive = extractBits(payload, 14, 1) === 1;
73
+ return {
74
+ active,
75
+ advisoryType: decodeAdvisoryType(active, corrective, downwardSense, increasedRate, senseReversal, altitudeCrossing, positive),
76
+ corrective,
77
+ downwardSense,
78
+ increasedRate,
79
+ senseReversal,
80
+ altitudeCrossing,
81
+ positive,
82
+ doNotPassBelow: extractBits(payload, 22, 1) === 1,
83
+ doNotPassAbove: extractBits(payload, 23, 1) === 1,
84
+ doNotTurnLeft: extractBits(payload, 24, 1) === 1,
85
+ doNotTurnRight: extractBits(payload, 25, 1) === 1,
86
+ terminated: extractBits(payload, 26, 1) === 1,
87
+ multipleThreat: extractBits(payload, 27, 1) === 1,
88
+ threat: decodeThreat(payload, threatTypeIndicator),
89
+ };
90
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Decodes a 13-bit Mode-S altitude code (the AC field of DF0/4/16/20
3
+ * surveillance replies) to feet, per ICAO Annex 10 Vol. IV / RTCA DO-260.
4
+ *
5
+ * The field supports two encodings, selected by the Q bit: a 25-foot
6
+ * linear encoding (Q=1), or a 100-foot Gillham (Gray code) encoding
7
+ * (Q=0) - the legacy scheme shared with Mode-C. Bit layout, MSB first:
8
+ * `C1 A1 C2 A2 C4 A4 M B1 Q B2 D2 B4 D4`.
9
+ *
10
+ * @param altitudeCode - The raw 13-bit AC field, 0-8191.
11
+ * @returns Altitude in feet, or undefined if the field is all-zero (altitude unknown), reports the rare M (metric) encoding, or contains an invalid Gillham value.
12
+ */
13
+ export declare function decodeAltitudeCode(altitudeCode: number): number | undefined;
14
+ /**
15
+ * Decodes the 12-bit altitude field of an ADS-B airborne position message
16
+ * (type codes 9-18, barometric altitude). The field omits the M bit that
17
+ * {@link decodeAltitudeCode}'s 13-bit AC field carries, so it is
18
+ * re-inserted (always 0 - the M encoding never appears in ADS-B position
19
+ * messages) before delegating to the shared Gillham/linear decoder.
20
+ *
21
+ * @param positionAltitudeField - The raw 12-bit altitude field from bits 8-19 of an airborne position ME field.
22
+ * @returns Altitude in feet, or undefined if unavailable.
23
+ */
24
+ export declare function decodeAdsbPositionAltitude(positionAltitudeField: number): number | undefined;
25
+ /**
26
+ * Decodes the GNSS altitude field of an ADS-B airborne position message
27
+ * (type codes 20-22). Unlike barometric altitude, this field is a plain
28
+ * 12-bit integer in meters - no Gillham or linear-code decoding involved.
29
+ *
30
+ * @param gnssAltitudeField - The raw 12-bit altitude field from bits 8-19 of a GNSS-altitude position ME field.
31
+ * @returns Altitude in feet.
32
+ */
33
+ export declare function decodeAdsbGnssAltitude(gnssAltitudeField: number): number;
34
+ //# sourceMappingURL=altitude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"altitude.d.ts","sourceRoot":"","sources":["../src/altitude.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAyD3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,qBAAqB,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG5F;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAExE"}
@@ -0,0 +1,99 @@
1
+ import { bitAt } from './bits.js';
2
+ /** 13-bit field width shared by the altitude (AC) and identity (ID) fields. */
3
+ const FIELD_WIDTH = 13;
4
+ /**
5
+ * Converts an unsigned Gillham code (a reflected Gray-code variant) to a
6
+ * plain binary integer via the standard Gray-to-binary fold.
7
+ */
8
+ function grayToBinary(value, bitWidth) {
9
+ let result = value;
10
+ for (let shift = 1; shift < bitWidth; shift *= 2) {
11
+ result ^= result >> shift;
12
+ }
13
+ return result;
14
+ }
15
+ /**
16
+ * Decodes a 13-bit Mode-S altitude code (the AC field of DF0/4/16/20
17
+ * surveillance replies) to feet, per ICAO Annex 10 Vol. IV / RTCA DO-260.
18
+ *
19
+ * The field supports two encodings, selected by the Q bit: a 25-foot
20
+ * linear encoding (Q=1), or a 100-foot Gillham (Gray code) encoding
21
+ * (Q=0) - the legacy scheme shared with Mode-C. Bit layout, MSB first:
22
+ * `C1 A1 C2 A2 C4 A4 M B1 Q B2 D2 B4 D4`.
23
+ *
24
+ * @param altitudeCode - The raw 13-bit AC field, 0-8191.
25
+ * @returns Altitude in feet, or undefined if the field is all-zero (altitude unknown), reports the rare M (metric) encoding, or contains an invalid Gillham value.
26
+ */
27
+ export function decodeAltitudeCode(altitudeCode) {
28
+ if (altitudeCode === 0) {
29
+ return undefined;
30
+ }
31
+ const bitOf = (pos) => bitAt(altitudeCode, pos, FIELD_WIDTH);
32
+ const mBit = bitOf(6);
33
+ const qBit = bitOf(8);
34
+ if (mBit === 0 && qBit === 1) {
35
+ // 25-foot linear encoding: drop the M and Q bits, the remaining 11 bits
36
+ // form the value - the top 6 bits (C1A1C2A2C4A4), then B1, then the
37
+ // bottom 4 bits (B2D2B4D4).
38
+ const topSixBits = (altitudeCode >> 7) & 0x3f;
39
+ const bottomFourBits = altitudeCode & 0xf;
40
+ const n = (topSixBits << 5) | (bitOf(7) << 4) | bottomFourBits;
41
+ return n * 25 - 1000;
42
+ }
43
+ if (mBit === 0 && qBit === 0) {
44
+ const c1 = bitOf(0);
45
+ const a1 = bitOf(1);
46
+ const c2 = bitOf(2);
47
+ const a2 = bitOf(3);
48
+ const c4 = bitOf(4);
49
+ const a4 = bitOf(5);
50
+ const b1 = bitOf(7);
51
+ const b2 = bitOf(9);
52
+ const d2 = bitOf(10);
53
+ const b4 = bitOf(11);
54
+ const d4 = bitOf(12);
55
+ // Re-order into an 8-bit 500 ft Gillham counter and a 3-bit 100 ft
56
+ // Gillham counter, per ICAO Annex 10 Vol. IV / DO-260.
57
+ const gray500 = (d2 << 7) | (d4 << 6) | (a1 << 5) | (a2 << 4) | (a4 << 3) | (b1 << 2) | (b2 << 1) | b4;
58
+ const gray100 = (c1 << 2) | (c2 << 1) | c4;
59
+ const n500 = grayToBinary(gray500, 8);
60
+ let n100 = grayToBinary(gray100, 4);
61
+ if (n100 === 0 || n100 === 5 || n100 === 6) {
62
+ return undefined;
63
+ }
64
+ if (n100 === 7) {
65
+ n100 = 5;
66
+ }
67
+ if (n500 % 2 === 1) {
68
+ n100 = 6 - n100;
69
+ }
70
+ return n500 * 500 + n100 * 100 - 1300;
71
+ }
72
+ // M = 1: rare, non-standard meter-based encoding. Not decoded.
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Decodes the 12-bit altitude field of an ADS-B airborne position message
77
+ * (type codes 9-18, barometric altitude). The field omits the M bit that
78
+ * {@link decodeAltitudeCode}'s 13-bit AC field carries, so it is
79
+ * re-inserted (always 0 - the M encoding never appears in ADS-B position
80
+ * messages) before delegating to the shared Gillham/linear decoder.
81
+ *
82
+ * @param positionAltitudeField - The raw 12-bit altitude field from bits 8-19 of an airborne position ME field.
83
+ * @returns Altitude in feet, or undefined if unavailable.
84
+ */
85
+ export function decodeAdsbPositionAltitude(positionAltitudeField) {
86
+ const altitudeCode = ((positionAltitudeField >> 6) << 7) | (positionAltitudeField & 0x3f);
87
+ return decodeAltitudeCode(altitudeCode);
88
+ }
89
+ /**
90
+ * Decodes the GNSS altitude field of an ADS-B airborne position message
91
+ * (type codes 20-22). Unlike barometric altitude, this field is a plain
92
+ * 12-bit integer in meters - no Gillham or linear-code decoding involved.
93
+ *
94
+ * @param gnssAltitudeField - The raw 12-bit altitude field from bits 8-19 of a GNSS-altitude position ME field.
95
+ * @returns Altitude in feet.
96
+ */
97
+ export function decodeAdsbGnssAltitude(gnssAltitudeField) {
98
+ return Math.round(gnssAltitudeField * 3.28084);
99
+ }
package/dist/bits.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extracts an unsigned integer from `bitLength` bits starting at `bitOffset`
3
+ * (0-indexed from the MSB of `bytes`), crossing byte boundaries as needed.
4
+ *
5
+ * JavaScript's bitwise operators work on 32-bit signed integers, so a raw
6
+ * Mode-S ME field (56 bits) can't be treated as one shifted integer the way
7
+ * a language with arbitrary-precision integers can. This reads bit by bit
8
+ * instead, safe for any field up to 31 bits - comfortably more than any
9
+ * single field this package decodes.
10
+ *
11
+ * @param bytes - The byte array to read from.
12
+ * @param bitOffset - Index of the first bit to read, 0-indexed from the MSB of `bytes[0]`.
13
+ * @param bitLength - Number of bits to read.
14
+ * @returns The extracted value as an unsigned integer.
15
+ */
16
+ export declare function extractBits(bytes: Uint8Array, bitOffset: number, bitLength: number): number;
17
+ /**
18
+ * Extracts a single bit directly from an already-extracted integer field,
19
+ * at `bitPosition` (0-indexed from the MSB of a `fieldWidth`-bit field).
20
+ * For fields already narrow enough to fit safely in a plain 32-bit
21
+ * bitwise op (comfortably true for anything this package decodes, e.g. a
22
+ * 13-bit identity or altitude code) - {@link extractBits}'s byte-array
23
+ * form exists to cross byte boundaries within a much wider ME field, which
24
+ * a single already-extracted small field never needs to do.
25
+ *
26
+ * @param value - The field's value, e.g. a 13-bit integer in [0, 8191].
27
+ * @param bitPosition - Index of the bit to read, 0-indexed from the MSB.
28
+ * @param fieldWidth - Total width of the field in bits.
29
+ * @returns The bit at that position, 0 or 1.
30
+ */
31
+ export declare function bitAt(value: number, bitPosition: number, fieldWidth: number): number;
32
+ /**
33
+ * Formats a 24-bit value (an ICAO address, a recovered CRC-XOR address, or
34
+ * any other 24-bit identifier this package handles) as 6 uppercase hex
35
+ * digits, zero-padded.
36
+ *
37
+ * @param value - The 24-bit value, 0-16777215.
38
+ * @returns The value as 6 uppercase hex digits.
39
+ */
40
+ export declare function formatHexAddress(value: number): string;
41
+ //# sourceMappingURL=bits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bits.d.ts","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAW3F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD"}
package/dist/bits.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Extracts an unsigned integer from `bitLength` bits starting at `bitOffset`
3
+ * (0-indexed from the MSB of `bytes`), crossing byte boundaries as needed.
4
+ *
5
+ * JavaScript's bitwise operators work on 32-bit signed integers, so a raw
6
+ * Mode-S ME field (56 bits) can't be treated as one shifted integer the way
7
+ * a language with arbitrary-precision integers can. This reads bit by bit
8
+ * instead, safe for any field up to 31 bits - comfortably more than any
9
+ * single field this package decodes.
10
+ *
11
+ * @param bytes - The byte array to read from.
12
+ * @param bitOffset - Index of the first bit to read, 0-indexed from the MSB of `bytes[0]`.
13
+ * @param bitLength - Number of bits to read.
14
+ * @returns The extracted value as an unsigned integer.
15
+ */
16
+ export function extractBits(bytes, bitOffset, bitLength) {
17
+ let value = 0;
18
+ for (let i = 0; i < bitLength; i++) {
19
+ const bitPos = bitOffset + i;
20
+ const byteIndex = bitPos >> 3;
21
+ const bitInByte = 7 - (bitPos & 7);
22
+ const byte = bytes[byteIndex] ?? 0;
23
+ const bit = (byte >> bitInByte) & 1;
24
+ value = (value << 1) | bit;
25
+ }
26
+ return value;
27
+ }
28
+ /**
29
+ * Extracts a single bit directly from an already-extracted integer field,
30
+ * at `bitPosition` (0-indexed from the MSB of a `fieldWidth`-bit field).
31
+ * For fields already narrow enough to fit safely in a plain 32-bit
32
+ * bitwise op (comfortably true for anything this package decodes, e.g. a
33
+ * 13-bit identity or altitude code) - {@link extractBits}'s byte-array
34
+ * form exists to cross byte boundaries within a much wider ME field, which
35
+ * a single already-extracted small field never needs to do.
36
+ *
37
+ * @param value - The field's value, e.g. a 13-bit integer in [0, 8191].
38
+ * @param bitPosition - Index of the bit to read, 0-indexed from the MSB.
39
+ * @param fieldWidth - Total width of the field in bits.
40
+ * @returns The bit at that position, 0 or 1.
41
+ */
42
+ export function bitAt(value, bitPosition, fieldWidth) {
43
+ return (value >> (fieldWidth - 1 - bitPosition)) & 1;
44
+ }
45
+ /**
46
+ * Formats a 24-bit value (an ICAO address, a recovered CRC-XOR address, or
47
+ * any other 24-bit identifier this package handles) as 6 uppercase hex
48
+ * digits, zero-padded.
49
+ *
50
+ * @param value - The 24-bit value, 0-16777215.
51
+ * @returns The value as 6 uppercase hex digits.
52
+ */
53
+ export function formatHexAddress(value) {
54
+ return value.toString(16).padStart(6, '0').toUpperCase();
55
+ }
@@ -0,0 +1,37 @@
1
+ import type { CommBRegister, HeadingAndSpeedReport, SelectedVerticalIntention, TrackAndTurnReport } from './types/index.js';
2
+ /**
3
+ * Decodes a BDS 4,0 (Selected Vertical Intention) register.
4
+ *
5
+ * @param mb - The 7-byte MB field of a DF20/21 Comm-B reply, already known (or inferred via {@link inferCommBRegisters}) to be BDS 4,0.
6
+ * @returns The decoded selected vertical intention.
7
+ */
8
+ export declare function decodeSelectedVerticalIntention(mb: Uint8Array): SelectedVerticalIntention;
9
+ /**
10
+ * Decodes a BDS 5,0 (Track and Turn Report) register.
11
+ *
12
+ * @param mb - The 7-byte MB field of a DF20/21 Comm-B reply, already known (or inferred via {@link inferCommBRegisters}) to be BDS 5,0.
13
+ * @returns The decoded track and turn report.
14
+ */
15
+ export declare function decodeTrackAndTurnReport(mb: Uint8Array): TrackAndTurnReport;
16
+ /**
17
+ * Decodes a BDS 6,0 (Heading and Speed Report) register.
18
+ *
19
+ * @param mb - The 7-byte MB field of a DF20/21 Comm-B reply, already known (or inferred via {@link inferCommBRegisters}) to be BDS 6,0.
20
+ * @returns The decoded heading and speed report.
21
+ */
22
+ export declare function decodeHeadingAndSpeedReport(mb: Uint8Array): HeadingAndSpeedReport;
23
+ /**
24
+ * Infers which "Enhanced Surveillance" Comm-B register(s) a DF20/21 MB
25
+ * field plausibly holds, and decodes each. Unlike DF17/18's type-code-coded
26
+ * ME field, a Comm-B MB field carries no self-declared register identifier
27
+ * for BDS 4,0/5,0/6,0 - this package would rather report every plausible
28
+ * candidate (via structural and range validation, matching pyModeS's own
29
+ * approach to this exact ambiguity) than silently guess a single wrong
30
+ * answer. In practice this is usually unambiguous; BDS 5,0 and 6,0 are the
31
+ * pair most likely to both pass validation for the same bytes.
32
+ *
33
+ * @param mb - The 7-byte MB field of a DF20/21 Comm-B reply.
34
+ * @returns Every register this MB field plausibly holds - empty if none, one if unambiguous, more than one if genuinely ambiguous.
35
+ */
36
+ export declare function inferCommBRegisters(mb: Uint8Array): CommBRegister[];
37
+ //# sourceMappingURL=comm-b.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comm-b.d.ts","sourceRoot":"","sources":["../src/comm-b.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AA6E1B;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,EAAE,EAAE,UAAU,GAAG,yBAAyB,CAkBzF;AA6CD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,UAAU,GAAG,kBAAkB,CA0B3E;AAoDD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,UAAU,GAAG,qBAAqB,CAoBjF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,EAAE,CAYnE"}