npm package: @hamradio/packet
Description: Low level packet parsing library (for radio protocols)
Author: Wijnand Modderman-Lenstra
Latest version: 1.1.1

# 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.

```ts
// 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`):

```ts
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:

```ts
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.