npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aeroswarm/codec

v0.1.10

Published

Decoding library for aviation surveillance protocols. Converts raw wire-format data (ASTERIX UDP datagrams) into structured records that the rest of the AeroSwarm pipeline normalizes into track events.

Readme

@aeroswarm/codec

Decoding library for aviation surveillance protocols. Converts raw wire-format data (ASTERIX UDP datagrams) into structured records that the rest of the AeroSwarm pipeline normalizes into track events.

Node-only. The decoder reads its XML format definitions from disk at runtime — never import this package from browser code. Frontends consume the already-normalized events via @aeroswarm/client.

Installation

pnpm add @aeroswarm/codec

Quick start

import { parseAsterix, resolveAsterixPosition, resolveAsterixAltitudeFt } from "@aeroswarm/codec";

socket.on("message", (buf: Buffer) => {
  // A single UDP datagram may contain multiple concatenated ASTERIX blocks —
  // parseAsterix handles that and returns one record per block.
  const records = parseAsterix(buf);

  for (const rec of records) {
    // rec.category  → 21, 23, 48, …
    // rec.edition   → which edition actually decoded it, e.g. "0.26"
    // rec.fields    → decoded values (icao24, lat/lon, callsign, …)
    const pos = resolveAsterixPosition(rec.fields, radarSite);
    const altFt = resolveAsterixAltitudeFt(rec.fields);
  }
});

Forcing a specific edition

Auto-detection tries every known edition per category and keeps the richest plausible decode (a wrong-edition UAP can still "successfully" decode with fields landing in the wrong slots, so first-match is not safe). When you know your sensor's edition, pin it — deterministic and one decode per packet instead of several:

// e.g. the kantor radar transmits CAT21 edition 0.26
const records = parseAsterix(buf, { 21: "0.26" });

In the services this is wired to the ASTERIX_EDITIONS env var (format "21:0.26,48:1.21").

Supported categories & editions

| Category | Content | Editions (newest first) | |----------|---------|--------------------------| | CAT010 | A-SMGCS monosensor surface movement (aircraft + vehicles) | 1.1 | | CAT011 | A-SMGCS fused surface movement | 1.2 | | CAT020 | Multilateration (MLAT) target reports | 1.7 | | CAT021 | ADS-B target reports | 2.4, 1.8, 1.4, 0.26 | | CAT023 | CNS/ATM ground station & service status | 1.2 | | CAT034 | Radar service messages (north marker, sector crossing) — sensor id only | 1.27, 1.26 | | CAT048 | Monoradar (PSR/SSR) target reports | 1.21, 1.14 | | CAT062 | System/fused tracks from ATC (keyed by system track number; item 380 is compound — only the target address ADR is unpacked into icao24) | 1.18, 1.16, 1.9, 1.7, 0.17 | | CAT063 | Sensor status — sensor id only | 1.3 |

CAT021 (edition 1.4+) and CAT048 also expose trackNumber (I021/161, I048/161), so reports without a Mode-S address can still be tracked per sensor.

Definitions are standard asterix XML files in src/config/. More category XMLs are already present there (CAT001, 002, 004, …) but not yet registered — see "Adding a category" below.

AFTN / ATS messages

Flight-plan data, not positions: parseAftn turns one raw AFTN message (FPL, CHG, DLA, DEP, ARR, CNL) into plain JSON.

import { parseAftn, splitAftnStream } from "@aeroswarm/codec";

// UDP: one message per datagram
const msg = parseAftn(datagram);            // AftnMessage | null
// msg.callsign "BTK7630" · msg.adep "WARR" · msg.ades "WIII" · msg.aircraftType "A320"
// msg.cruiseSpeed "N0450" · msg.cruiseLevel "F350" · msg.route "DCT ABMAL L897 BIKMA DCT"
// msg.icao24 "8a0123" (item 18 CODE/) · msg.eobtAt / atdAt / ataAt (epoch ms, UTC)

// TCP: a message ends at NNNN or ETX
const { messages, rest } = splitAftnStream(buffered + chunk);
  • Tolerates gateway framing: SOH/STX/ETX, a ZCZC start line, NNNN, unknown header lines. Returns null when no ( … ) body is present — never throws.
  • Month and year are resolved from DOF/ or, failing that, the origin DDHHMM group nearest to options.now.
  • icao24 comes from CODE/ and is the precise key for correlating a plan with a surveillance track; callsign is the fallback.
  • Route stays a string (airways, fixes, DCT, coordinates) — turning fixes into coordinates needs an AIRAC database this package does not carry.

Exposed API

parseAsterix(buf, defaultEdition?)

function parseAsterix(
  buf: Buffer,
  defaultEdition?: Record<number, string>, // category → edition, skips auto-detect
): AsterixRecord[]

Parses one UDP datagram (possibly multiple concatenated blocks) into records. Never throws on malformed input — bad blocks are skipped and an empty array is a valid result. Records failing the plausibility check (lat/lon out of range) are dropped.

resolveAsterixPosition(fields, radarSite?)

Returns { lat, lon } | null. Uses WGS-84 position when present; for radar categories reporting range/azimuth (measuredPosition) or cartesian offsets (positionCartesian), converts relative to radarSite.

resolveAsterixAltitudeFt(fields)

Returns the best available altitude in feet (geometric → flight level preference) or undefined.

Types

interface AsterixRecord { category: number; edition: string; fields: AsterixFields; }

interface AsterixFields {
  icao24?: string;            // 6-char hex aircraft address
  callsign?: string;          // e.g. "MAS179"
  squawk?: string;            // 4-digit octal
  lat?: number; lon?: number; // WGS-84 degrees
  altitudeFt?: number;
  timeOfDay?: number;         // seconds since UTC midnight
  trackNumber?: number;       // per-sensor track number (CAT010/011/020/021/048/062)
  velocity?: { groundSpeedKt?: number; headingDeg?: number };
  positionCartesian?: { xNm?: number; yNm?: number };
  measuredPosition?: { rangeNm?: number; azimuthDeg?: number };
  dataSource?: { sac: number; sic: number };
  // CAT23 ground-station status
  reportType?: number;        // 1 = ground station status
  gsStatus?: { nogo: boolean; odp: boolean; oxt: boolean; msc: boolean; tsv: boolean; spo: boolean };
  operationalRangeNm?: number;
}

interface RadarSite { lat: number; lon: number; }

Per-category subpath re-exports

cat10/, cat21/, cat48/, cat62/ re-export the same parser API for callers that want category-scoped imports.

Other protocol entry points (stubs)

decodeAdsbFrame, decodeAis, decodeMlatFrame, decodeAsmgcsFrame exist as placeholders and currently return null. Raw ADS-B/AIS decoding is not implemented — ADS-B arrives via ASTERIX CAT21.

How decoding works (architecture)

XML definition (src/config/asterix_catXXX_Y_Z.xml)
  ├─ UAP        → which FSPEC bit maps to which DataItem
  ├─ formats    → fixed/variable/repetitive/compound/explicit item lengths
  └─ <Bits>     → named bit-fields: range, signedness, encoding, scale
        ↓
decoder.ts   walks FSPEC + items, slices each present item's bytes
values.ts    extractBitFields() turns bytes into named raw values per the XML
fields.ts    semantic mapper: raw names → canonical AsterixFields
             (XML-first; falls back to manual byte decoding when a
              definition lacks usable <Bits> specs)
parser.ts    edition selection: forced, or richest-plausible-decode wins

Value extraction is config-driven: bit positions, two's-complement handling, scale factors (180/2²⁵ vs 180/2²³ per edition, etc.), 6-bit ICAO callsign chars, octal squawk — all read from the XML. The semantic layer stays as a thin, reviewable mapping because "which item is the position" and altitude-preference rules are policy, not format.

Adding a category or edition

  1. Drop the standard asterix XML into src/config/ (many are already there).
  2. Register it in FILE_MAP (xml-loader.ts) and EDITIONS (parser.ts).
  3. If the category introduces new semantic fields, extend AsterixFields (types.ts) and add a small mapping in fields.ts.
  4. Add a regression test with a real captured packet in parser.test.ts.

Testing

pnpm test

Tests use real packets captured from the live radar feed as fixtures (a CAT21/0.26 MAS179 report and a CAT23 ground-station status), plus multi-block and malformed-input cases. When touching the decoder, always keep these green — they pin exact decoded values.

Low-level API (advanced)

Exposed for tooling/tests; not needed for normal decoding:

| Export | Purpose | |--------|---------| | loadDef(cat, edition) | Load + cache one XML definition (AsterixDef: UAP + item formats + bit-fields) | | decodeBlock(block, def, onField) | Walk one ASTERIX block, invoking onField(id, bytes) per present item | | parseFspec(buf, start?) | Parse an FX-extended FSPEC into a bit array | | extractBitFields(data, defs) | Apply BitFieldDef[] to raw item bytes → named values | | BitFieldDef, ItemDef, AsterixDef, UAPEntry, RawValues | Supporting types |