@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/dist/frame.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The 24-bit Mode-S CRC generator polynomial (ICAO Annex 10 Vol. IV / RTCA
3
+ * DO-260), represented as its low 24 bits - the implicit leading term
4
+ * cancels against the overflow bit it is XORed in response to.
5
+ */
6
+ const CRC24_GENERATOR = 0xfff409;
7
+ /**
8
+ * Extracts the downlink format from a Mode-S message's first 5 bits.
9
+ *
10
+ * @param bytes - Raw message bytes (7 or 14 bytes).
11
+ * @returns The raw downlink format value, 0-31.
12
+ */
13
+ export function extractDownlinkFormat(bytes) {
14
+ const first = bytes[0] ?? 0;
15
+ return first >> 3;
16
+ }
17
+ /**
18
+ * Computes the 24-bit Mode-S CRC remainder over an entire message (all
19
+ * bytes, including the trailing 24-bit parity field), via the standard
20
+ * bit-serial polynomial division used to check or recover Mode-S CRCs.
21
+ *
22
+ * See {@link ModeSMessageEnvelope.crcRemainder} for how to interpret the
23
+ * result - it is a pass/fail check for squitter formats (DF11/17/18) but a
24
+ * recovered ICAO address for surveillance-reply formats.
25
+ *
26
+ * @param bytes - Raw message bytes (7 or 14 bytes), parity field included.
27
+ * @returns The 24-bit CRC remainder.
28
+ */
29
+ export function computeCrc24(bytes) {
30
+ let register = 0;
31
+ for (const byte of bytes) {
32
+ for (let bitIndex = 7; bitIndex >= 0; bitIndex--) {
33
+ const bit = (byte >> bitIndex) & 1;
34
+ const topBit = (register >> 23) & 1;
35
+ register = ((register << 1) | bit) & 0xffffff;
36
+ if (topBit === 1) {
37
+ register ^= CRC24_GENERATOR;
38
+ }
39
+ }
40
+ }
41
+ return register;
42
+ }
43
+ /**
44
+ * Parses the shared envelope (downlink format, CRC remainder) out of a raw
45
+ * Mode-S message. {@link decodeModeSMessage} uses this to decide how to
46
+ * route a message before calling the per-type decoders - those decoders
47
+ * take a raw ME field or an already-extracted field value, not the
48
+ * envelope itself.
49
+ *
50
+ * ```typescript
51
+ * import { parseModeSFrame } from '@squawk/mode-s';
52
+ *
53
+ * const envelope = parseModeSFrame(rawMessageBytes);
54
+ * if (envelope.downlinkFormat === 17 && envelope.crcRemainder === 0) {
55
+ * // unmodified DF17 extended squitter, safe to decode further
56
+ * }
57
+ * ```
58
+ *
59
+ * @param bytes - Raw message bytes (7 or 14 bytes).
60
+ * @returns The parsed envelope.
61
+ */
62
+ export function parseModeSFrame(bytes) {
63
+ return {
64
+ bytes,
65
+ downlinkFormat: extractDownlinkFormat(bytes),
66
+ crcRemainder: computeCrc24(bytes),
67
+ };
68
+ }
@@ -0,0 +1,10 @@
1
+ import type { AircraftIdentification } from './types/index.js';
2
+ /**
3
+ * Decodes an ADS-B aircraft identification message (BDS 0,8, type codes
4
+ * 1-4).
5
+ *
6
+ * @param me - The 7-byte ME field of a DF17/18 message whose type code is 1-4.
7
+ * @returns The decoded callsign and category.
8
+ */
9
+ export declare function decodeIdentification(me: Uint8Array): AircraftIdentification;
10
+ //# sourceMappingURL=identification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identification.d.ts","sourceRoot":"","sources":["../src/identification.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAoD/D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,GAAG,sBAAsB,CAQ3E"}
@@ -0,0 +1,64 @@
1
+ import { AircraftCategory } from '@squawk/types';
2
+ import { extractBits } from './bits.js';
3
+ /**
4
+ * The 64-entry ICAO Mode-S callsign alphabet, a truncation of ASCII: 1-26
5
+ * map to 'A'-'Z', 32 is space, 48-57 are '0'-'9', everything else is
6
+ * invalid. Built once from the ASCII rule rather than hand-typed, so there
7
+ * is no separate string literal that could drift from the rule itself.
8
+ */
9
+ const CALLSIGN_ALPHABET = buildCallsignAlphabet();
10
+ function buildCallsignAlphabet() {
11
+ let table = '';
12
+ for (let i = 0; i < 64; i++) {
13
+ if (i >= 1 && i <= 26) {
14
+ table += String.fromCharCode(i | 0x40);
15
+ }
16
+ else if (i === 32 || (i >= 48 && i <= 57)) {
17
+ table += String.fromCharCode(i);
18
+ }
19
+ else {
20
+ table += '#';
21
+ }
22
+ }
23
+ return table;
24
+ }
25
+ function decodeCallsign(me) {
26
+ let callsign = '';
27
+ for (let i = 0; i < 8; i++) {
28
+ const charIndex = extractBits(me, 8 + i * 6, 6);
29
+ callsign += CALLSIGN_ALPHABET[charIndex] ?? '#';
30
+ }
31
+ const trimmed = callsign.trim();
32
+ return trimmed.length > 0 ? trimmed : undefined;
33
+ }
34
+ function isAircraftCategoryCode(value) {
35
+ return Object.hasOwn(AircraftCategory, value);
36
+ }
37
+ /**
38
+ * Maps a type code's category "set" letter (per DO-260B Table A-2-8) and
39
+ * the 3-bit category subfield to squawk's `AircraftCategory` key, e.g.
40
+ * type code 4 category 5 -> `"A5"` (heavy). Type code 1 and category 0
41
+ * both mean "no category information" and have no key.
42
+ */
43
+ function categoryKeyFor(typeCode, category) {
44
+ if (category === 0) {
45
+ return undefined;
46
+ }
47
+ const setLetter = typeCode === 4 ? 'A' : typeCode === 3 ? 'B' : typeCode === 2 ? 'C' : undefined;
48
+ return setLetter === undefined ? undefined : `${setLetter}${category}`;
49
+ }
50
+ /**
51
+ * Decodes an ADS-B aircraft identification message (BDS 0,8, type codes
52
+ * 1-4).
53
+ *
54
+ * @param me - The 7-byte ME field of a DF17/18 message whose type code is 1-4.
55
+ * @returns The decoded callsign and category.
56
+ */
57
+ export function decodeIdentification(me) {
58
+ const typeCode = extractBits(me, 0, 5);
59
+ const categoryBits = extractBits(me, 5, 3);
60
+ const callsign = decodeCallsign(me);
61
+ const key = categoryKeyFor(typeCode, categoryBits);
62
+ const category = key !== undefined && isAircraftCategoryCode(key) ? AircraftCategory[key] : undefined;
63
+ return { callsign, category };
64
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Decodes a 13-bit Mode-S identity (ID) field to a 4-digit octal squawk
3
+ * string, per ICAO Annex 10 Vol. IV / RTCA DO-260. The field packs each
4
+ * octal digit across three interleaved pulse bits (e.g. digit A from bits
5
+ * A4, A2, A1) rather than storing the four digits contiguously.
6
+ *
7
+ * @param idField - The raw 13-bit ID field, 0-8191.
8
+ * @returns The 4-character octal squawk string, e.g. "1200".
9
+ */
10
+ export declare function decodeIdentityCode(idField: number): string;
11
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAS1D"}
@@ -0,0 +1,20 @@
1
+ import { bitAt } from './bits.js';
2
+ /** 13-bit field width shared by the identity (ID) and altitude (AC) fields. */
3
+ const FIELD_WIDTH = 13;
4
+ /**
5
+ * Decodes a 13-bit Mode-S identity (ID) field to a 4-digit octal squawk
6
+ * string, per ICAO Annex 10 Vol. IV / RTCA DO-260. The field packs each
7
+ * octal digit across three interleaved pulse bits (e.g. digit A from bits
8
+ * A4, A2, A1) rather than storing the four digits contiguously.
9
+ *
10
+ * @param idField - The raw 13-bit ID field, 0-8191.
11
+ * @returns The 4-character octal squawk string, e.g. "1200".
12
+ */
13
+ export function decodeIdentityCode(idField) {
14
+ const bitOf = (pos) => bitAt(idField, pos, FIELD_WIDTH);
15
+ const a = (bitOf(5) << 2) | (bitOf(3) << 1) | bitOf(1); // A4 A2 A1
16
+ const b = (bitOf(11) << 2) | (bitOf(9) << 1) | bitOf(7); // B4 B2 B1
17
+ const c = (bitOf(4) << 2) | (bitOf(2) << 1) | bitOf(0); // C4 C2 C1
18
+ const d = (bitOf(12) << 2) | (bitOf(10) << 1) | bitOf(8); // D4 D2 D1
19
+ return `${a}${b}${c}${d}`;
20
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * Decode raw Mode-S/ADS-B messages: downlink format and CRC extraction,
4
+ * CPR position, airborne velocity, identification, and altitude decoding.
5
+ * Transport-agnostic - operates on already-framed message bytes regardless
6
+ * of where they came from (Beast, a logged capture, or any other source).
7
+ */
8
+ export { decodeModeSMessage } from './message.js';
9
+ export { decodeAcasResolutionAdvisory } from './acas.js';
10
+ export { decodeAdsbGnssAltitude, decodeAdsbPositionAltitude, decodeAltitudeCode, } from './altitude.js';
11
+ export { decodeHeadingAndSpeedReport, decodeSelectedVerticalIntention, decodeTrackAndTurnReport, inferCommBRegisters, } from './comm-b.js';
12
+ export { cprNumLongitudeZones, decodeAirborneCprPair, decodeAirborneCprWithReference, decodeSurfaceCprPair, decodeSurfaceCprWithReference, } from './cpr.js';
13
+ export { decodeEmergencyState } from './emergency-status.js';
14
+ export { extractDownlinkFormat, computeCrc24, parseModeSFrame } from './frame.js';
15
+ export { decodeIdentification } from './identification.js';
16
+ export { decodeIdentityCode } from './identity.js';
17
+ export { decodeModeAc } from './mode-ac.js';
18
+ export { decodeAircraftOperationalStatus } from './operational-status.js';
19
+ export { decodeSurfaceMovement } from './surface-movement.js';
20
+ export { decodeTargetStateAndStatus } from './target-state-status.js';
21
+ export { decodeAirborneVelocity } from './velocity.js';
22
+ export type { AllCallReply, CommBAltitudeReply, CommBIdentityReply, DecodedModeSMessage, ExtendedSquitterAcasRaBroadcast, ExtendedSquitterCommon, ExtendedSquitterEmergencyStatus, ExtendedSquitterIdentification, ExtendedSquitterOperationalStatus, ExtendedSquitterPosition, ExtendedSquitterTargetStateAndStatus, ExtendedSquitterVelocity, LongAirAirSurveillanceReply, MessageSource, ShortAirAirSurveillanceReply, SurveillanceAltitudeReply, SurveillanceIdentityReply, } from './types/index.js';
23
+ export type { AircraftOperationalStatus } from './types/index.js';
24
+ export type { TargetStateAndStatus } from './types/index.js';
25
+ export type { CommBRegister, HeadingAndSpeedReport, SelectedVerticalIntention, TrackAndTurnReport, } from './types/index.js';
26
+ export type { AcasResolutionAdvisoryReport, AcasThreat, AcasThreatAltitudeRangeBearing, AcasThreatIcaoAddress, AcasThreatNone, AcasThreatType, ResolutionAdvisoryType, } from './types/index.js';
27
+ export type { EmergencyState } from './types/index.js';
28
+ export type { AircraftIdentification } from './types/index.js';
29
+ export type { AirborneVelocity, AirborneVelocityCommon, AirSpeedVelocity, GroundSpeedVelocity, } from './types/index.js';
30
+ export type { CprPosition, CprReference, ModeSMessageEnvelope } from './types/index.js';
31
+ export type { ModeAcReply } from './types/index.js';
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EACL,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,EAC/B,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,8BAA8B,EAC9B,oBAAoB,EACpB,6BAA6B,GAC9B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,+BAA+B,EAC/B,sBAAsB,EACtB,+BAA+B,EAC/B,8BAA8B,EAC9B,iCAAiC,EACjC,wBAAwB,EACxB,oCAAoC,EACpC,wBAAwB,EACxB,2BAA2B,EAC3B,aAAa,EACb,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,4BAA4B,EAC5B,UAAU,EACV,8BAA8B,EAC9B,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxF,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * Decode raw Mode-S/ADS-B messages: downlink format and CRC extraction,
4
+ * CPR position, airborne velocity, identification, and altitude decoding.
5
+ * Transport-agnostic - operates on already-framed message bytes regardless
6
+ * of where they came from (Beast, a logged capture, or any other source).
7
+ */
8
+ export { decodeModeSMessage } from './message.js';
9
+ export { decodeAcasResolutionAdvisory } from './acas.js';
10
+ export { decodeAdsbGnssAltitude, decodeAdsbPositionAltitude, decodeAltitudeCode, } from './altitude.js';
11
+ export { decodeHeadingAndSpeedReport, decodeSelectedVerticalIntention, decodeTrackAndTurnReport, inferCommBRegisters, } from './comm-b.js';
12
+ export { cprNumLongitudeZones, decodeAirborneCprPair, decodeAirborneCprWithReference, decodeSurfaceCprPair, decodeSurfaceCprWithReference, } from './cpr.js';
13
+ export { decodeEmergencyState } from './emergency-status.js';
14
+ export { extractDownlinkFormat, computeCrc24, parseModeSFrame } from './frame.js';
15
+ export { decodeIdentification } from './identification.js';
16
+ export { decodeIdentityCode } from './identity.js';
17
+ export { decodeModeAc } from './mode-ac.js';
18
+ export { decodeAircraftOperationalStatus } from './operational-status.js';
19
+ export { decodeSurfaceMovement } from './surface-movement.js';
20
+ export { decodeTargetStateAndStatus } from './target-state-status.js';
21
+ export { decodeAirborneVelocity } from './velocity.js';
@@ -0,0 +1,40 @@
1
+ import type { DecodedModeSMessage } from './types/index.js';
2
+ /**
3
+ * Decodes a raw Mode-S message into its typed, discriminated result.
4
+ *
5
+ * A downlink format value of 16 or higher always denotes a 112-bit (14
6
+ * byte) long message; below 16 always denotes a 56-bit (7 byte) short
7
+ * message - this holds for every downlink format, decoded or not. Any
8
+ * `bytes` whose length does not match what its own downlink format implies
9
+ * is rejected before further decoding, since a mismatch means the buffer is
10
+ * truncated or corrupted (e.g. a bit-flipped DF field misreading a short
11
+ * reply as a long one) rather than a genuine message of that format.
12
+ *
13
+ * DF17/18 (extended squitter) messages are only decoded when their CRC is
14
+ * exactly zero - a non-zero CRC on a squitter means the message was
15
+ * corrupted in transit, and this package would rather report "not
16
+ * decodable" than return plausible-looking but wrong data. DF18 messages
17
+ * are further gated on the control field: only CF=0/1/2/5/6 (which share
18
+ * DF17's type-code-coded ME layout) are decoded - see {@link MessageSource},
19
+ * which every decoded DF17/18 message carries so callers can tell a real,
20
+ * direct ICAO address from an anonymous or ground-derived one rather than
21
+ * losing that distinction after the gate. DF24 (Comm-D) is recognized
22
+ * but not decoded - see {@link DecodedModeSMessage}. DF19 (military
23
+ * extended squitter) and DF22 (military use) are reserved formats with no
24
+ * publicly documented payload and are not decoded, for the same reason as
25
+ * DF24.
26
+ *
27
+ * ```typescript
28
+ * import { decodeModeSMessage } from '@squawk/mode-s';
29
+ *
30
+ * const decoded = decodeModeSMessage(rawMessageBytes);
31
+ * if (decoded?.kind === 'extendedSquitterPosition') {
32
+ * console.log(decoded.icaoHex, decoded.latCpr, decoded.lonCpr);
33
+ * }
34
+ * ```
35
+ *
36
+ * @param bytes - Raw message bytes (7 or 14 bytes).
37
+ * @returns The decoded message, or undefined if `bytes`' length does not match its downlink format, the downlink format is unrecognized, not decoded by this package, or the message fails its CRC check.
38
+ */
39
+ export declare function decodeModeSMessage(bytes: Uint8Array): DecodedModeSMessage | undefined;
40
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../src/message.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,mBAAmB,EAGpB,MAAM,kBAAkB,CAAC;AAoN1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,mBAAmB,GAAG,SAAS,CAqFrF"}
@@ -0,0 +1,293 @@
1
+ import { decodeAcasResolutionAdvisory } from './acas.js';
2
+ import { decodeAdsbGnssAltitude, decodeAdsbPositionAltitude, decodeAltitudeCode, } from './altitude.js';
3
+ import { extractBits, formatHexAddress } from './bits.js';
4
+ import { inferCommBRegisters } from './comm-b.js';
5
+ import { decodeEmergencyState } from './emergency-status.js';
6
+ import { parseModeSFrame } from './frame.js';
7
+ import { decodeIdentification } from './identification.js';
8
+ import { decodeIdentityCode } from './identity.js';
9
+ import { decodeAircraftOperationalStatus } from './operational-status.js';
10
+ import { decodeSurfaceMovement } from './surface-movement.js';
11
+ import { decodeTargetStateAndStatus } from './target-state-status.js';
12
+ import { decodeAirborneVelocity } from './velocity.js';
13
+ /**
14
+ * DF11's CRC remainder carries a 7-bit interrogator code field: a 1-bit IC
15
+ * selector plus a 6-bit value, either a legacy 4-bit II code (IC=1, low 2
16
+ * bits unused) or a denser 6-bit SI code (IC=0) used where more than 16
17
+ * co-located interrogators are in play - real values up to 127 are
18
+ * legitimate. A larger remainder than this suggests a corrupted message
19
+ * rather than a legitimate code.
20
+ */
21
+ const MAX_PLAUSIBLE_INTERROGATOR_CODE = 127;
22
+ const TYPE_CODE_CATEGORIES = [
23
+ { min: 0, max: 0, category: 'airborneBaroPosition' },
24
+ { min: 1, max: 4, category: 'identification' },
25
+ { min: 5, max: 8, category: 'surfacePosition' },
26
+ { min: 9, max: 18, category: 'airborneBaroPosition' },
27
+ { min: 19, max: 19, category: 'velocity' },
28
+ { min: 20, max: 22, category: 'airborneGnssPosition' },
29
+ { min: 28, max: 28, category: 'aircraftStatus' },
30
+ { min: 29, max: 29, category: 'targetStateAndStatus' },
31
+ { min: 31, max: 31, category: 'operationalStatus' },
32
+ ];
33
+ function categorizeTypeCode(typeCode) {
34
+ return TYPE_CODE_CATEGORIES.find(({ min, max }) => typeCode >= min && typeCode <= max)?.category;
35
+ }
36
+ /**
37
+ * Maps a DF18 control-field (CF) value to the {@link MessageSource} it
38
+ * denotes - the single source of truth for both which CF values this
39
+ * package decodes (those with a defined case below) and what they mean.
40
+ * CF=3 (TIS-B, coarse format) uses a materially different field layout
41
+ * this package does not decode; CF=4 (TIS-B management) and CF=7
42
+ * (reserved) carry no per-aircraft state to decode - both fall through
43
+ * to `undefined`, same as an unrecognized value.
44
+ */
45
+ function messageSourceForControlField(controlField) {
46
+ switch (controlField) {
47
+ case 0:
48
+ return 'icaoDirect';
49
+ case 1:
50
+ return 'anonymousDirect';
51
+ case 2:
52
+ return 'icaoTisb';
53
+ case 5:
54
+ return 'anonymousTisb';
55
+ case 6:
56
+ return 'adsr';
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
61
+ function hexAddress(bytes, byteOffset) {
62
+ return formatHexAddress(extractBits(bytes, byteOffset * 8, 24));
63
+ }
64
+ /** Decodes the fields shared by DF0 and DF16 (both ACAS/TCAS air-air surveillance replies): recovered address, on-ground status, and altitude. DF16 additionally carries a Resolution Advisory report on top of this. */
65
+ function decodeAirAirSurveillanceCore(bytes, crcRemainder) {
66
+ const acField = extractBits(bytes, 19, 13);
67
+ return {
68
+ candidateIcaoHex: formatHexAddress(crcRemainder),
69
+ surface: extractBits(bytes, 5, 1) === 1,
70
+ altitudeFt: decodeAltitudeCode(acField),
71
+ };
72
+ }
73
+ /**
74
+ * Decodes the three position-carrying categories (surface, airborne baro,
75
+ * airborne GNSS) - identical CPR field layout, differing only in what
76
+ * replaces the altitude bits for a surface message. Type code 0 (airborne,
77
+ * no position information) shares `airborneBaroPosition`'s altitude-field
78
+ * layout, but its CPR fields are defined as unavailable rather than a real
79
+ * encoded position, so `hasPosition` lets the caller omit them rather than
80
+ * exposing plausible-looking-but-meaningless bits.
81
+ */
82
+ function decodePositionMessage(me, icaoHex, messageSource, hasPosition, category) {
83
+ const isSurface = category === 'surfacePosition';
84
+ const cprFormat = extractBits(me, 21, 1) === 1 ? 'odd' : 'even';
85
+ const latCpr = hasPosition ? extractBits(me, 22, 17) : undefined;
86
+ const lonCpr = hasPosition ? extractBits(me, 39, 17) : undefined;
87
+ let altitudeFt;
88
+ let groundSpeedKt;
89
+ let trueTrackDeg;
90
+ if (isSurface) {
91
+ // Surface messages replace the airborne altitude field (bits 8-19)
92
+ // with movement (bits 5-11) and track (bits 12-19) - a different
93
+ // field layout, not just a different interpretation of the same bits.
94
+ const movementField = extractBits(me, 5, 7);
95
+ const trackStatus = extractBits(me, 12, 1);
96
+ const trackRaw = extractBits(me, 13, 7);
97
+ groundSpeedKt = decodeSurfaceMovement(movementField);
98
+ trueTrackDeg = trackStatus === 1 ? (trackRaw * 360) / 128 : undefined;
99
+ }
100
+ else {
101
+ const altitudeField = extractBits(me, 8, 12);
102
+ altitudeFt =
103
+ category === 'airborneBaroPosition'
104
+ ? decodeAdsbPositionAltitude(altitudeField)
105
+ : decodeAdsbGnssAltitude(altitudeField);
106
+ }
107
+ return {
108
+ kind: 'extendedSquitterPosition',
109
+ icaoHex,
110
+ messageSource,
111
+ surface: isSurface,
112
+ cprFormat,
113
+ latCpr,
114
+ lonCpr,
115
+ altitudeFt,
116
+ groundSpeedKt,
117
+ trueTrackDeg,
118
+ };
119
+ }
120
+ function decodeExtendedSquitter(bytes, icaoHex, messageSource) {
121
+ const me = bytes.slice(4, 11);
122
+ const typeCode = extractBits(me, 0, 5);
123
+ const category = categorizeTypeCode(typeCode);
124
+ switch (category) {
125
+ case 'identification':
126
+ return {
127
+ kind: 'extendedSquitterIdentification',
128
+ icaoHex,
129
+ messageSource,
130
+ identification: decodeIdentification(me),
131
+ };
132
+ case 'velocity': {
133
+ const velocity = decodeAirborneVelocity(me);
134
+ return velocity === undefined
135
+ ? undefined
136
+ : { kind: 'extendedSquitterVelocity', icaoHex, messageSource, velocity };
137
+ }
138
+ case 'targetStateAndStatus':
139
+ return {
140
+ kind: 'extendedSquitterTargetStateAndStatus',
141
+ icaoHex,
142
+ messageSource,
143
+ targetStateAndStatus: decodeTargetStateAndStatus(me),
144
+ };
145
+ case 'operationalStatus':
146
+ return {
147
+ kind: 'extendedSquitterOperationalStatus',
148
+ icaoHex,
149
+ messageSource,
150
+ operationalStatus: decodeAircraftOperationalStatus(me),
151
+ };
152
+ case 'aircraftStatus': {
153
+ const subtype = extractBits(me, 5, 3);
154
+ if (subtype === 1) {
155
+ const emergencyState = decodeEmergencyState(extractBits(me, 8, 3));
156
+ const idField = extractBits(me, 11, 13);
157
+ return {
158
+ kind: 'extendedSquitterEmergencyStatus',
159
+ icaoHex,
160
+ messageSource,
161
+ emergencyState,
162
+ squawk: decodeIdentityCode(idField),
163
+ };
164
+ }
165
+ if (subtype === 2) {
166
+ const resolutionAdvisory = decodeAcasResolutionAdvisory(me);
167
+ return resolutionAdvisory === undefined
168
+ ? undefined
169
+ : { kind: 'extendedSquitterAcasRaBroadcast', icaoHex, messageSource, resolutionAdvisory };
170
+ }
171
+ return undefined;
172
+ }
173
+ case 'surfacePosition':
174
+ case 'airborneBaroPosition':
175
+ case 'airborneGnssPosition':
176
+ return decodePositionMessage(me, icaoHex, messageSource, typeCode !== 0, category);
177
+ default:
178
+ return undefined;
179
+ }
180
+ }
181
+ /**
182
+ * Decodes a raw Mode-S message into its typed, discriminated result.
183
+ *
184
+ * A downlink format value of 16 or higher always denotes a 112-bit (14
185
+ * byte) long message; below 16 always denotes a 56-bit (7 byte) short
186
+ * message - this holds for every downlink format, decoded or not. Any
187
+ * `bytes` whose length does not match what its own downlink format implies
188
+ * is rejected before further decoding, since a mismatch means the buffer is
189
+ * truncated or corrupted (e.g. a bit-flipped DF field misreading a short
190
+ * reply as a long one) rather than a genuine message of that format.
191
+ *
192
+ * DF17/18 (extended squitter) messages are only decoded when their CRC is
193
+ * exactly zero - a non-zero CRC on a squitter means the message was
194
+ * corrupted in transit, and this package would rather report "not
195
+ * decodable" than return plausible-looking but wrong data. DF18 messages
196
+ * are further gated on the control field: only CF=0/1/2/5/6 (which share
197
+ * DF17's type-code-coded ME layout) are decoded - see {@link MessageSource},
198
+ * which every decoded DF17/18 message carries so callers can tell a real,
199
+ * direct ICAO address from an anonymous or ground-derived one rather than
200
+ * losing that distinction after the gate. DF24 (Comm-D) is recognized
201
+ * but not decoded - see {@link DecodedModeSMessage}. DF19 (military
202
+ * extended squitter) and DF22 (military use) are reserved formats with no
203
+ * publicly documented payload and are not decoded, for the same reason as
204
+ * DF24.
205
+ *
206
+ * ```typescript
207
+ * import { decodeModeSMessage } from '@squawk/mode-s';
208
+ *
209
+ * const decoded = decodeModeSMessage(rawMessageBytes);
210
+ * if (decoded?.kind === 'extendedSquitterPosition') {
211
+ * console.log(decoded.icaoHex, decoded.latCpr, decoded.lonCpr);
212
+ * }
213
+ * ```
214
+ *
215
+ * @param bytes - Raw message bytes (7 or 14 bytes).
216
+ * @returns The decoded message, or undefined if `bytes`' length does not match its downlink format, the downlink format is unrecognized, not decoded by this package, or the message fails its CRC check.
217
+ */
218
+ export function decodeModeSMessage(bytes) {
219
+ const envelope = parseModeSFrame(bytes);
220
+ const expectedLength = envelope.downlinkFormat >= 16 ? 14 : 7;
221
+ if (bytes.length !== expectedLength) {
222
+ return undefined;
223
+ }
224
+ if (envelope.downlinkFormat === 17 || envelope.downlinkFormat === 18) {
225
+ if (envelope.crcRemainder !== 0) {
226
+ return undefined;
227
+ }
228
+ // DF17 carries no control field - a DF17 transponder is always
229
+ // ICAO-addressed and transmitting its own state directly, by protocol
230
+ // definition, so this is a fixed value rather than a decoded one.
231
+ const messageSource = envelope.downlinkFormat === 17
232
+ ? 'icaoDirect'
233
+ : messageSourceForControlField(extractBits(bytes, 5, 3));
234
+ if (messageSource === undefined) {
235
+ return undefined;
236
+ }
237
+ return decodeExtendedSquitter(bytes, hexAddress(bytes, 1), messageSource);
238
+ }
239
+ if (envelope.downlinkFormat === 11) {
240
+ if (envelope.crcRemainder > MAX_PLAUSIBLE_INTERROGATOR_CODE) {
241
+ return undefined;
242
+ }
243
+ return { kind: 'allCallReply', icaoHex: hexAddress(bytes, 1) };
244
+ }
245
+ if (envelope.downlinkFormat === 0) {
246
+ return {
247
+ kind: 'shortAirAirSurveillanceReply',
248
+ ...decodeAirAirSurveillanceCore(bytes, envelope.crcRemainder),
249
+ };
250
+ }
251
+ if (envelope.downlinkFormat === 16) {
252
+ return {
253
+ kind: 'longAirAirSurveillanceReply',
254
+ ...decodeAirAirSurveillanceCore(bytes, envelope.crcRemainder),
255
+ resolutionAdvisory: decodeAcasResolutionAdvisory(bytes.slice(4, 11)),
256
+ };
257
+ }
258
+ if (envelope.downlinkFormat === 4) {
259
+ const acField = extractBits(bytes, 19, 13);
260
+ return {
261
+ kind: 'surveillanceAltitudeReply',
262
+ candidateIcaoHex: formatHexAddress(envelope.crcRemainder),
263
+ altitudeFt: decodeAltitudeCode(acField),
264
+ };
265
+ }
266
+ if (envelope.downlinkFormat === 20) {
267
+ const acField = extractBits(bytes, 19, 13);
268
+ return {
269
+ kind: 'commBAltitudeReply',
270
+ candidateIcaoHex: formatHexAddress(envelope.crcRemainder),
271
+ altitudeFt: decodeAltitudeCode(acField),
272
+ commBRegisters: inferCommBRegisters(bytes.slice(4, 11)),
273
+ };
274
+ }
275
+ if (envelope.downlinkFormat === 5) {
276
+ const idField = extractBits(bytes, 19, 13);
277
+ return {
278
+ kind: 'surveillanceIdentityReply',
279
+ candidateIcaoHex: formatHexAddress(envelope.crcRemainder),
280
+ squawk: decodeIdentityCode(idField),
281
+ };
282
+ }
283
+ if (envelope.downlinkFormat === 21) {
284
+ const idField = extractBits(bytes, 19, 13);
285
+ return {
286
+ kind: 'commBIdentityReply',
287
+ candidateIcaoHex: formatHexAddress(envelope.crcRemainder),
288
+ squawk: decodeIdentityCode(idField),
289
+ commBRegisters: inferCommBRegisters(bytes.slice(4, 11)),
290
+ };
291
+ }
292
+ return undefined;
293
+ }
@@ -0,0 +1,17 @@
1
+ import type { ModeAcReply } from './types/index.js';
2
+ /**
3
+ * Decodes a Mode A/C reply, as carried by a Beast type-1 frame. Beast
4
+ * represents the reply as the raw big-endian 16-bit value dump1090-fa
5
+ * calls "ModeA" internally: each of the four squawk digits occupies one
6
+ * nibble (masking with `0x7777` isolates them), with the Ident pulse and a
7
+ * couple of spare bits interspersed.
8
+ *
9
+ * Mode A/C carries no ICAO address, so a decoded reply cannot be
10
+ * associated with a specific aircraft the way a Mode-S message can - it is
11
+ * decode-only in this package, not wired into any aircraft-tracking state.
12
+ *
13
+ * @param bytes - The 2-byte Mode A/C payload from a Beast type-1 frame.
14
+ * @returns The decoded squawk code, Ident flag, and altitude (if the reply looks like a valid Mode C report).
15
+ */
16
+ export declare function decodeModeAc(bytes: Uint8Array): ModeAcReply;
17
+ //# sourceMappingURL=mode-ac.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode-ac.d.ts","sourceRoot":"","sources":["../src/mode-ac.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAsEpD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAO3D"}