2026-03-12 17:11:18 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:11:18 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00
2026-03-12 17:07:09 +01:00

packet.js — Packet parsing

This project exposes lightweight types (FieldType, Field, Segment, Dissected, BitField) and a Reader/Writer API to read/write binary packet data sequentially.

Below is a quick guide showing how to use the types together with Reader to parse a packet and store parsed fields as a Dissected structure.

Concepts

  • FieldType: string constants describing how bytes should be interpreted (UINT8, UINT16_LE, BYTES, UTF8_STRING, BITS, etc.).
  • Field: describes a single named datum in a Segment (type, name, optional length or bit definitions).
  • Segment: a named collection of fields; segments form the Dissected result.
  • Dissected: an array of Segment objects that represent the parsed packet.
  • Reader: sequential reader that advances an offset and exposes methods (uint8, uint16, uint64, bytes, utf8String, cString, words, dwords, qwords, etc.) and performs bounds checking.

Example: APRS (AX.25 UI-frame) parsing

APRS (Automatic Packet Reporting System) commonly rides on AX.25 UI-frames used by amateur radio. AX.25 address blocks are fixed-width (7 bytes per address) and the frame typically contains:

  • Destination address (7 bytes)
  • Source address (7 bytes)
  • Optional digipeaters (n * 7 bytes)
  • Control (1 byte, usually 0x03 for UI)
  • PID (1 byte, usually 0xF0)
  • Information field (UTF-8 text, remaining bytes) — APRS payload

Below is a minimal TypeScript example showing how to parse an AX.25 UI-frame binary into a Dissected structure using Reader. The example assumes the frame has a single destination, single source, no digipeaters, and a UTF-8 info field for simplicity.

// Example segment description (informational only)
const ax25Segment = {
  name: 'ax25_ui_frame',
  fields: [
    { type: FieldType.BYTES, name: 'dst_addr', length: 7 },
    { type: FieldType.BYTES, name: 'src_addr', length: 7 },
    { type: FieldType.UINT8, name: 'control' },
    { type: FieldType.UINT8, name: 'pid' },
    { type: FieldType.UTF8_STRING, name: 'info', length: undefined } // rest of buffer
  ]
};

Parsing function (using Reader):

import { Reader } from './src/index';
import { FieldType, Dissected, Segment } from './src/types';

/**
 * Parse a raw AX.25 UI-frame ArrayBuffer into a Dissected structure.
 */
function parseAX25UIFrame(buf: ArrayBuffer): Dissected {
  const r = new Reader(buf); // defaults to LittleEndian where applicable
  const seg: Segment = { name: 'ax25_ui_frame', fields: [] };

  // destination address (7 bytes)
  const dst = r.bytes(7);
  seg.fields.push({ name: 'dst_addr', type: FieldType.BYTES, length: 7 });
  // store raw data for convenience (optional) — store a slice of the
  // underlying buffer for just this field rather than copying the entire buffer.
  seg.data = dst.buffer.slice(dst.byteOffset, dst.byteOffset + dst.byteLength);

  // source address (7 bytes)
  const src = r.bytes(7);
  seg.fields.push({ name: 'src_addr', type: FieldType.BYTES, length: 7 });

  // control (1 byte)
  const control = r.uint8();
  seg.fields.push({ name: 'control', type: FieldType.UINT8 });

  // pid (1 byte)
  const pid = r.uint8();
  seg.fields.push({ name: 'pid', type: FieldType.UINT8 });

  // info: remaining bytes decoded as UTF-8
  const info = r.utf8String(); // reads to end by default
  seg.fields.push({ name: 'info', type: FieldType.UTF8_STRING });

  // return a Dissected array containing the single segment
  return [seg];
}

Notes on storing parsed values:

  • The Segment and Field types describe the structure. You can store actual parsed values by using the Field.value property or by keeping a parallel Map/Object for the parsed values.
  • In the example above, seg.fields declare the shape. To keep parsed values, add a values property:
seg.values = {
  dst_addr: new Uint8Array(dst),
  src_addr: new Uint8Array(src),
  control,
  pid,
  info
};

Handling bit fields and arrays

  • Use FieldType.BITS plus Field.bits for bit-level definitions. Read underlying bytes with bytes() or uint8() and extract bits per BitField definitions. -- For arrays (WORDS, DWORDS, QWORDS, BYTES) set Field.length and use the corresponding Reader method (words, dwords, qwords, bytes). Note: the words/dwords/qwords helpers create aligned copies and return typed arrays that are safe to use on all platforms.

Helpers and test tips:

  • Use Reader.fromBytes() / Reader.fromString() to construct readers from convenience formats.
  • Use Writer.toBytes() to obtain the written bytes; tests should prefer this method instead of reaching into Writer internals.

Best practices for radio parsing

  • Always perform bounds checks (Reader does this internally via checkBounds).
  • Validate control/PID bytes (AX.25 UI typically uses 0x03 and 0xF0).
  • When parsing addresses in AX.25, remember each address byte is 7-bit ASCII shifted and the last address byte has the "end" flag; real-world parsing needs address unpacking logic (not shown in the minimal example).

Summary

  • The provided types describe packet structure; Reader reads primitives and advances offset safely.
  • Compose Segment arrays (Dissected) while parsing to represent packet structure and store parsed values next to their field descriptors for debugging, display, or serialization.
  • The APRS/AX.25 example demonstrates how a short radio protocol payload can be parsed into a structured Dissected result suitable for display or further processing.
Description
Low level packet parsing library (for radio protocols) in Typescript
https://www.npmjs.com/package/@hamradio/packet
Readme 148 KiB
Languages
TypeScript 100%