@onion-party/spacepackets 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ryangibbs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ ![spacepackets](logo.png)
2
+
3
+ # spacepackets
4
+
5
+ [![CI](https://github.com/ryangibbs/spacepackets/actions/workflows/ci.yml/badge.svg)](https://github.com/ryangibbs/spacepackets/actions/workflows/ci.yml)
6
+
7
+ A TypeScript library for encoding and decoding CCSDS Space Packets — the standard packet format used in spacecraft telemetry and telecommand systems ([CCSDS 133.0-B-2](https://public.ccsds.org/Pubs/133x0b2e1.pdf)).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @onion-party/spacepackets
13
+ # or
14
+ pnpm add @onion-party/spacepackets
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ### Decode a telemetry packet
20
+
21
+ ```ts
22
+ import { decode, PacketType } from "@onion-party/spacepackets";
23
+
24
+ const bytes = new Uint8Array([/* raw packet bytes from socket or file */]);
25
+ const packet = decode(bytes);
26
+
27
+ console.log(packet.header.apid); // Application Process ID
28
+ console.log(packet.header.sequenceCount); // For gap detection
29
+ console.log(packet.header.packetType); // PacketType.Telemetry or PacketType.Telecommand
30
+ console.log(packet.dataField); // Uint8Array — secondary header + user data
31
+ ```
32
+
33
+ ### Encode a telecommand
34
+
35
+ ```ts
36
+ import { encode, PacketType, SequenceFlags } from "@onion-party/spacepackets";
37
+
38
+ const packet = {
39
+ header: {
40
+ packetVersionNumber: 0,
41
+ packetType: PacketType.Telecommand,
42
+ secondaryHeaderFlag: false,
43
+ apid: 100,
44
+ sequenceFlags: SequenceFlags.Standalone,
45
+ sequenceCount: 0,
46
+ dataLength: 2, // dataField.byteLength - 1
47
+ },
48
+ dataField: new Uint8Array([0x01, 0x02, 0x03]),
49
+ };
50
+
51
+ const bytes = encode(packet); // Uint8Array ready for transmission
52
+ ```
53
+
54
+ ### Assemble packets from a byte stream
55
+
56
+ Packets often arrive in chunks — split across frame boundaries or delivered over a socket. `PacketAssembler` buffers incoming bytes and emits complete packets.
57
+
58
+ ```ts
59
+ import { PacketAssembler } from "@onion-party/spacepackets";
60
+
61
+ const assembler = new PacketAssembler();
62
+
63
+ // Feed raw bytes as they arrive (e.g. from a UDP socket)
64
+ socket.on("data", (chunk: Buffer) => {
65
+ const packets = assembler.onChunk(chunk);
66
+ for (const packet of packets) {
67
+ console.log("received packet", packet.header.apid);
68
+ }
69
+ });
70
+ ```
71
+
72
+ ### Full pipeline (Node.js streams)
73
+
74
+ All three stream classes compose via `.pipe()` for a fully declarative receive pipeline:
75
+
76
+ ```ts
77
+ import {
78
+ PacketAssemblerStream,
79
+ GapDetectorStream,
80
+ SegmentationReassemblerStream,
81
+ type ReassembledAdu,
82
+ } from "@onion-party/spacepackets";
83
+
84
+ socket
85
+ .pipe(new PacketAssemblerStream({ maxBufferBytes: 1_000_000 }))
86
+ .pipe(new GapDetectorStream({
87
+ onGap: (gap) => console.warn(`APID ${gap.apid}: ${gap.missing} packet(s) missing`),
88
+ }))
89
+ .pipe(new SegmentationReassemblerStream({
90
+ onError: (err) => console.warn(`APID ${err.apid}: ${err.kind}`),
91
+ }))
92
+ .on("data", (adu: ReassembledAdu) => {
93
+ console.log(`APID ${adu.apid}: ${adu.data.byteLength} bytes`);
94
+ });
95
+ ```
96
+
97
+ ### Detect gaps in the packet stream
98
+
99
+ `GapDetector` tracks the sequence count per APID and fires a callback whenever packets are missing.
100
+
101
+ ```ts
102
+ import { GapDetector, PacketAssembler } from "@onion-party/spacepackets";
103
+
104
+ const assembler = new PacketAssembler();
105
+ const detector = new GapDetector({
106
+ onGap: (gap) => {
107
+ console.warn(`APID ${gap.apid}: expected ${gap.expected}, got ${gap.received} — ${gap.missing} packet(s) missing`);
108
+ },
109
+ });
110
+
111
+ socket.on("data", (chunk: Buffer) => {
112
+ const packets = assembler.onChunk(chunk);
113
+ for (const packet of packets) {
114
+ detector.onPacket(packet.header);
115
+ }
116
+ });
117
+ ```
118
+
119
+ ### Reassemble segmented ADUs
120
+
121
+ Large application data units (ADUs) are sometimes split across multiple packets using `First`/`Continuation`/`Last` sequence flags. `SegmentationReassembler` buffers segments per APID and emits the fully concatenated data when the final segment arrives.
122
+
123
+ ```ts
124
+ import { PacketAssembler, SegmentationReassembler } from "@onion-party/spacepackets";
125
+
126
+ const assembler = new PacketAssembler();
127
+ const reassembler = new SegmentationReassembler({
128
+ onAdu: (adu) => {
129
+ console.log(`APID ${adu.apid}: reassembled ${adu.data.byteLength} bytes`);
130
+ },
131
+ onError: (err) => {
132
+ if (err.kind === 'ORPHANED_SEGMENT') {
133
+ console.warn(`APID ${err.apid}: segment arrived with no matching First packet`);
134
+ } else {
135
+ console.warn(`APID ${err.apid}: new First arrived while ADU was in-progress — abandoned`);
136
+ }
137
+ },
138
+ });
139
+
140
+ socket.on("data", (chunk: Buffer) => {
141
+ const packets = assembler.onChunk(chunk);
142
+ for (const packet of packets) {
143
+ reassembler.onPacket(packet);
144
+ }
145
+ });
146
+ ```
147
+
148
+ ### Error handling
149
+
150
+ All errors thrown by this library are instances of `SpacePacketError`. Use the `code` field to branch programmatically without parsing message strings.
151
+
152
+ ```ts
153
+ import { decode, SpacePacketError } from "@onion-party/spacepackets";
154
+
155
+ try {
156
+ const packet = decode(bytes);
157
+ } catch (err) {
158
+ if (err instanceof SpacePacketError) {
159
+ switch (err.code) {
160
+ case "BUFFER_TOO_SHORT":
161
+ // Not enough bytes yet — wait for more data
162
+ break;
163
+ case "INVALID_VERSION":
164
+ // Packet version number is non-zero — malformed or unsupported
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## API
172
+
173
+ ### Functions
174
+
175
+ | Function | Description |
176
+ |---|---|
177
+ | `encode(packet)` | Serialize a `SpacePacket` to a `Uint8Array` |
178
+ | `decode(bytes)` | Parse a `Uint8Array` into a `SpacePacket` |
179
+ | `decodeHeader(bytes)` | Parse only the 6-byte primary header — useful in streaming contexts to learn the full packet length before the payload has arrived |
180
+ | `getPacketLength(header)` | Returns the total byte length of a packet given its header (`6 + header.dataLength + 1`) |
181
+ | `isIdlePacket(header)` | Returns `true` if the packet's APID is `0x7FF` (the reserved idle/fill APID); idle packets carry no mission data and should be discarded |
182
+
183
+ ### Classes
184
+
185
+ | Class | Description |
186
+ |---|---|
187
+ | `PacketAssembler` | Stateful buffer that accepts raw byte chunks via `onChunk(chunk)` and returns complete `SpacePacket[]`. Handles packets split across chunk boundaries. |
188
+ | `GapDetector` | Tracks sequence counts per APID and calls `onGap` whenever packets are missing. Handles 14-bit wrap-around and ignores idle packets. |
189
+ | `SegmentationReassembler` | Reassembles segmented ADUs from `First`/`Continuation`/`Last` packet sequences. Calls `onAdu` when complete, `onError` on out-of-order or orphaned segments. |
190
+ | `PacketAssemblerStream` | Node.js `Transform` stream wrapping `PacketAssembler`. Accepts raw byte chunks on the writable side, emits decoded `SpacePacket` objects on the readable side. Emits `INCOMPLETE_PACKET` if the stream ends mid-packet. |
191
+ | `GapDetectorStream` | Passthrough `Transform` stream wrapping `GapDetector`. Accepts and re-emits `SpacePacket` objects; calls `onGap` as a side effect for any detected sequence gaps. |
192
+ | `SegmentationReassemblerStream` | `Transform` stream wrapping `SegmentationReassembler`. Accepts `SpacePacket` objects, emits `ReassembledAdu` objects when a complete ADU is assembled. |
193
+ | `SpacePacketError` | Thrown by `encode`/`decode` on malformed input, or emitted by `PacketAssemblerStream`. Has a `code` field: `BUFFER_TOO_SHORT`, `INVALID_VERSION`, `APID_OUT_OF_RANGE`, `DATA_FIELD_EMPTY`, `DATA_LENGTH_MISMATCH`, `SEQUENCE_COUNT_OUT_OF_RANGE`, `PACKET_TOO_LARGE`, `BUFFER_OVERFLOW`, `INCOMPLETE_PACKET`. |
194
+
195
+ ## Specification
196
+
197
+ This library implements [CCSDS 133.0-B-2 — Space Packet Protocol](https://public.ccsds.org/Pubs/133x0b2e1.pdf), published by the [Consultative Committee for Space Data Systems (CCSDS)](https://public.ccsds.org).
198
+
199
+ ## License
200
+
201
+ MIT
@@ -0,0 +1,28 @@
1
+ import { type PrimaryHeader, type SpacePacket } from './types.js';
2
+ /**
3
+ * Parses only the 6-byte primary header from the start of `bytes`.
4
+ *
5
+ * Useful in streaming contexts: read the header first to learn `header.dataLength`,
6
+ * then wait for the remaining `header.dataLength + 1` bytes before calling `decode`.
7
+ *
8
+ * Throws {@link SpacePacketError} if:
9
+ * - `bytes.byteLength < 6` (`BUFFER_TOO_SHORT`)
10
+ * - The packet version number field is not `0` (`INVALID_VERSION`)
11
+ */
12
+ export declare function decodeHeader(bytes: Uint8Array): PrimaryHeader;
13
+ /**
14
+ * Parses a complete space packet from `bytes`.
15
+ *
16
+ * Decodes the primary header, then slices out the data field.
17
+ * If `bytes` contains more data than one packet (e.g. a stream of concatenated packets),
18
+ * only the first packet is decoded — use `header.dataLength` to advance your read cursor.
19
+ *
20
+ * The returned `dataField` is an independent copy (`.slice()`), not a view into `bytes`,
21
+ * so mutating the input buffer after decoding is safe.
22
+ *
23
+ * Throws {@link SpacePacketError} if:
24
+ * - `bytes.byteLength < 6` (`BUFFER_TOO_SHORT`)
25
+ * - The packet version number field is not `0` (`INVALID_VERSION`)
26
+ * - `bytes.byteLength < 6 + header.dataLength + 1` (`BUFFER_TOO_SHORT`)
27
+ */
28
+ export declare function decode(bytes: Uint8Array): SpacePacket;
package/dist/decode.js ADDED
@@ -0,0 +1,63 @@
1
+ import assert from 'assert';
2
+ import { HEADER_BYTE_LENGTH } from './types.js';
3
+ import { SpacePacketError } from './errors.js';
4
+ import { getPacketLength } from './util.js';
5
+ /**
6
+ * Parses only the 6-byte primary header from the start of `bytes`.
7
+ *
8
+ * Useful in streaming contexts: read the header first to learn `header.dataLength`,
9
+ * then wait for the remaining `header.dataLength + 1` bytes before calling `decode`.
10
+ *
11
+ * Throws {@link SpacePacketError} if:
12
+ * - `bytes.byteLength < 6` (`BUFFER_TOO_SHORT`)
13
+ * - The packet version number field is not `0` (`INVALID_VERSION`)
14
+ */
15
+ export function decodeHeader(bytes) {
16
+ assert(bytes.byteLength >= HEADER_BYTE_LENGTH, SpacePacketError.bufferTooShort());
17
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
18
+ const byte1 = view.getUint8(0);
19
+ const packetVersionNumber = (byte1 >> 5) & 0x7;
20
+ assert(packetVersionNumber === 0, SpacePacketError.invalidVersion());
21
+ const packetType = (byte1 >> 4) & 0x1;
22
+ const secondaryHeaderFlag = ((byte1 >> 3) & 0x1) === 1;
23
+ const apid = ((byte1 & 0x7) << 8) | view.getUint8(1);
24
+ const byte2 = view.getUint8(2);
25
+ const sequenceFlags = (byte2 >> 6) & 0x3;
26
+ const byte3 = view.getUint8(3);
27
+ const sequenceCount = ((byte2 & 0x3f) << 8) | byte3;
28
+ const dataLength = view.getUint16(4); // bytes 4-5 as big-endian uint16
29
+ return {
30
+ packetVersionNumber: packetVersionNumber,
31
+ packetType,
32
+ secondaryHeaderFlag,
33
+ apid,
34
+ sequenceFlags,
35
+ sequenceCount,
36
+ dataLength,
37
+ };
38
+ }
39
+ /**
40
+ * Parses a complete space packet from `bytes`.
41
+ *
42
+ * Decodes the primary header, then slices out the data field.
43
+ * If `bytes` contains more data than one packet (e.g. a stream of concatenated packets),
44
+ * only the first packet is decoded — use `header.dataLength` to advance your read cursor.
45
+ *
46
+ * The returned `dataField` is an independent copy (`.slice()`), not a view into `bytes`,
47
+ * so mutating the input buffer after decoding is safe.
48
+ *
49
+ * Throws {@link SpacePacketError} if:
50
+ * - `bytes.byteLength < 6` (`BUFFER_TOO_SHORT`)
51
+ * - The packet version number field is not `0` (`INVALID_VERSION`)
52
+ * - `bytes.byteLength < 6 + header.dataLength + 1` (`BUFFER_TOO_SHORT`)
53
+ */
54
+ export function decode(bytes) {
55
+ const header = decodeHeader(bytes);
56
+ const totalLength = getPacketLength(header);
57
+ assert(bytes.byteLength >= totalLength, SpacePacketError.bufferTooShort());
58
+ const dataField = bytes.slice(HEADER_BYTE_LENGTH, totalLength);
59
+ return {
60
+ header,
61
+ dataField,
62
+ };
63
+ }
@@ -0,0 +1,16 @@
1
+ import { type SpacePacket } from './types.js';
2
+ /**
3
+ * Serializes a space packet to a byte array suitable for transmission.
4
+ *
5
+ * The returned buffer is: 6 header bytes + `packet.dataField`.
6
+ *
7
+ * Throws {@link SpacePacketError} if:
8
+ * - `header.packetVersionNumber` is not `0` (`INVALID_VERSION`)
9
+ * - `header.apid` is outside 0–2047 (`APID_OUT_OF_RANGE`)
10
+ * - `header.sequenceCount` is outside 0–16383 (`SEQUENCE_COUNT_OUT_OF_RANGE`)
11
+ * - `dataField` is empty (`DATA_FIELD_EMPTY`)
12
+ * - `dataField.byteLength` exceeds `MAX_DATA_FIELD_LENGTH` (65536) (`PACKET_TOO_LARGE`)
13
+ * - `header.dataLength` does not equal `dataField.byteLength - 1` (`DATA_LENGTH_MISMATCH`)
14
+ */
15
+ export declare function encode(packet: SpacePacket): Uint8Array;
16
+ export declare function encodeHeader(header: SpacePacket['header']): Uint8Array;
package/dist/encode.js ADDED
@@ -0,0 +1,57 @@
1
+ import { SpacePacketError } from './errors.js';
2
+ import { MAX_DATA_FIELD_LENGTH } from './types.js';
3
+ /**
4
+ * Serializes a space packet to a byte array suitable for transmission.
5
+ *
6
+ * The returned buffer is: 6 header bytes + `packet.dataField`.
7
+ *
8
+ * Throws {@link SpacePacketError} if:
9
+ * - `header.packetVersionNumber` is not `0` (`INVALID_VERSION`)
10
+ * - `header.apid` is outside 0–2047 (`APID_OUT_OF_RANGE`)
11
+ * - `header.sequenceCount` is outside 0–16383 (`SEQUENCE_COUNT_OUT_OF_RANGE`)
12
+ * - `dataField` is empty (`DATA_FIELD_EMPTY`)
13
+ * - `dataField.byteLength` exceeds `MAX_DATA_FIELD_LENGTH` (65536) (`PACKET_TOO_LARGE`)
14
+ * - `header.dataLength` does not equal `dataField.byteLength - 1` (`DATA_LENGTH_MISMATCH`)
15
+ */
16
+ export function encode(packet) {
17
+ const { header, dataField } = packet;
18
+ const headerBytes = encodeHeader(header);
19
+ if (dataField.byteLength === 0) {
20
+ throw SpacePacketError.dataFieldEmpty();
21
+ }
22
+ if (dataField.byteLength > MAX_DATA_FIELD_LENGTH) {
23
+ throw SpacePacketError.packetTooLarge();
24
+ }
25
+ if (packet.header.dataLength !== dataField.byteLength - 1) {
26
+ throw SpacePacketError.dataLengthMismatch();
27
+ }
28
+ const bytes = new Uint8Array(headerBytes.byteLength + dataField.byteLength);
29
+ bytes.set(headerBytes, 0);
30
+ bytes.set(dataField, headerBytes.byteLength);
31
+ return bytes;
32
+ }
33
+ export function encodeHeader(header) {
34
+ if (header.packetVersionNumber !== 0) {
35
+ throw SpacePacketError.invalidVersion();
36
+ }
37
+ if (header.apid < 0 || header.apid > 2047) {
38
+ throw SpacePacketError.apidOutOfRange();
39
+ }
40
+ if (header.sequenceCount < 0 || header.sequenceCount > 16383) {
41
+ throw SpacePacketError.sequenceCountOutOfRange();
42
+ }
43
+ const bytes = new Uint8Array(6);
44
+ const view = new DataView(bytes.buffer);
45
+ let byte1 = (header.packetVersionNumber & 0x7) << 5;
46
+ byte1 |= (header.packetType & 0x1) << 4;
47
+ byte1 |= (header.secondaryHeaderFlag ? 1 : 0) << 3;
48
+ byte1 |= (header.apid >> 8) & 0x7;
49
+ view.setUint8(0, byte1);
50
+ view.setUint8(1, header.apid & 0xff);
51
+ let byte2 = (header.sequenceFlags & 0x3) << 6;
52
+ byte2 |= (header.sequenceCount >> 8) & 0x3f;
53
+ view.setUint8(2, byte2);
54
+ view.setUint8(3, header.sequenceCount & 0xff);
55
+ view.setUint16(4, header.dataLength); // big-endian by default
56
+ return bytes;
57
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Thrown by encode/decode when a packet is malformed or a buffer is invalid.
3
+ * Use the `code` field to branch programmatically without parsing message strings.
4
+ */
5
+ export declare class SpacePacketError extends Error {
6
+ readonly code: 'BUFFER_TOO_SHORT' | 'INVALID_VERSION' | 'APID_OUT_OF_RANGE' | 'DATA_FIELD_EMPTY' | 'DATA_LENGTH_MISMATCH' | 'SEQUENCE_COUNT_OUT_OF_RANGE' | 'PACKET_TOO_LARGE' | 'BUFFER_OVERFLOW' | 'INCOMPLETE_PACKET';
7
+ constructor(message: string, code: 'BUFFER_TOO_SHORT' | 'INVALID_VERSION' | 'APID_OUT_OF_RANGE' | 'DATA_FIELD_EMPTY' | 'DATA_LENGTH_MISMATCH' | 'SEQUENCE_COUNT_OUT_OF_RANGE' | 'PACKET_TOO_LARGE' | 'BUFFER_OVERFLOW' | 'INCOMPLETE_PACKET');
8
+ static bufferTooShort(): SpacePacketError;
9
+ static invalidVersion(): SpacePacketError;
10
+ static apidOutOfRange(): SpacePacketError;
11
+ static dataFieldEmpty(): SpacePacketError;
12
+ static dataLengthMismatch(): SpacePacketError;
13
+ static sequenceCountOutOfRange(): SpacePacketError;
14
+ static packetTooLarge(): SpacePacketError;
15
+ static bufferOverflow(): SpacePacketError;
16
+ static incompletePacket(): SpacePacketError;
17
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Thrown by encode/decode when a packet is malformed or a buffer is invalid.
3
+ * Use the `code` field to branch programmatically without parsing message strings.
4
+ */
5
+ export class SpacePacketError extends Error {
6
+ code;
7
+ constructor(message, code) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = 'SpacePacketError';
11
+ }
12
+ static bufferTooShort() {
13
+ return new SpacePacketError('Buffer too short', 'BUFFER_TOO_SHORT');
14
+ }
15
+ static invalidVersion() {
16
+ return new SpacePacketError('Invalid packet version number', 'INVALID_VERSION');
17
+ }
18
+ static apidOutOfRange() {
19
+ return new SpacePacketError('APID out of range (0-2047)', 'APID_OUT_OF_RANGE');
20
+ }
21
+ static dataFieldEmpty() {
22
+ return new SpacePacketError('Data field is empty', 'DATA_FIELD_EMPTY');
23
+ }
24
+ static dataLengthMismatch() {
25
+ return new SpacePacketError('Data length mismatch', 'DATA_LENGTH_MISMATCH');
26
+ }
27
+ static sequenceCountOutOfRange() {
28
+ return new SpacePacketError('Sequence count out of range (0-16383)', 'SEQUENCE_COUNT_OUT_OF_RANGE');
29
+ }
30
+ static packetTooLarge() {
31
+ return new SpacePacketError('Packet too large: data field exceeds 65536 bytes (CCSDS maximum)', 'PACKET_TOO_LARGE');
32
+ }
33
+ static bufferOverflow() {
34
+ return new SpacePacketError('Buffer overflow: accumulated bytes exceed maxBufferBytes limit', 'BUFFER_OVERFLOW');
35
+ }
36
+ static incompletePacket() {
37
+ return new SpacePacketError('Incomplete packet', 'INCOMPLETE_PACKET');
38
+ }
39
+ }
@@ -0,0 +1,20 @@
1
+ import { type GapDetectorOptions, type PrimaryHeader } from './types.js';
2
+ export declare class GapDetector {
3
+ private readonly onGap;
4
+ /** Tracks the last received sequence count per APID. */
5
+ private readonly lastSeen;
6
+ constructor(options: GapDetectorOptions);
7
+ /**
8
+ * Inspects a packet header for sequence count gaps.
9
+ * Call this for every packet received, in order.
10
+ *
11
+ * - Idle packets (APID `0x7FF`) are silently ignored.
12
+ * - The first packet seen for an APID establishes the baseline; no gap is reported.
13
+ * - For subsequent packets, if `sequenceCount` is not `(lastSeen + 1) % SEQUENCE_COUNT_RANGE`,
14
+ * `onGap` is called with the details.
15
+ * - `missing` is the number of dropped packets:
16
+ * `(received - expected + SEQUENCE_COUNT_RANGE) % SEQUENCE_COUNT_RANGE`
17
+ */
18
+ onPacket(header: PrimaryHeader): void;
19
+ reset(): void;
20
+ }
@@ -0,0 +1,44 @@
1
+ import { IDLE_APID, SEQUENCE_COUNT_RANGE, } from './types.js';
2
+ export class GapDetector {
3
+ onGap;
4
+ /** Tracks the last received sequence count per APID. */
5
+ lastSeen = new Map();
6
+ constructor(options) {
7
+ this.onGap = options.onGap;
8
+ }
9
+ /**
10
+ * Inspects a packet header for sequence count gaps.
11
+ * Call this for every packet received, in order.
12
+ *
13
+ * - Idle packets (APID `0x7FF`) are silently ignored.
14
+ * - The first packet seen for an APID establishes the baseline; no gap is reported.
15
+ * - For subsequent packets, if `sequenceCount` is not `(lastSeen + 1) % SEQUENCE_COUNT_RANGE`,
16
+ * `onGap` is called with the details.
17
+ * - `missing` is the number of dropped packets:
18
+ * `(received - expected + SEQUENCE_COUNT_RANGE) % SEQUENCE_COUNT_RANGE`
19
+ */
20
+ onPacket(header) {
21
+ if (header.apid === IDLE_APID) {
22
+ return;
23
+ }
24
+ const last = this.lastSeen.get(header.apid);
25
+ if (last === undefined) {
26
+ this.lastSeen.set(header.apid, header.sequenceCount);
27
+ return;
28
+ }
29
+ const expected = (last + 1) % SEQUENCE_COUNT_RANGE;
30
+ if (header.sequenceCount !== expected) {
31
+ const gap = {
32
+ apid: header.apid,
33
+ expected,
34
+ received: header.sequenceCount,
35
+ missing: (header.sequenceCount - expected + SEQUENCE_COUNT_RANGE) % SEQUENCE_COUNT_RANGE,
36
+ };
37
+ this.onGap(gap);
38
+ }
39
+ this.lastSeen.set(header.apid, header.sequenceCount);
40
+ }
41
+ reset() {
42
+ this.lastSeen.clear();
43
+ }
44
+ }
@@ -0,0 +1,22 @@
1
+ import { Transform, type TransformCallback } from 'node:stream';
2
+ import type { GapDetectorStreamOptions, SpacePacket } from './types.js';
3
+ /**
4
+ * A passthrough Node.js `Transform` stream that wraps {@link GapDetector}.
5
+ *
6
+ * - **Writable side**: accepts decoded {@link SpacePacket} objects.
7
+ * - **Readable side**: emits the same {@link SpacePacket} objects unchanged.
8
+ * - **Side effect**: calls `onGap` for any detected sequence count gaps.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * socket
13
+ * .pipe(new PacketAssemblerStream())
14
+ * .pipe(new GapDetectorStream({ onGap: (gap) => console.warn(gap) }))
15
+ * .on('data', (packet: SpacePacket) => process(packet));
16
+ * ```
17
+ */
18
+ export declare class GapDetectorStream extends Transform {
19
+ private readonly detector;
20
+ constructor(options: GapDetectorStreamOptions);
21
+ _transform(packet: SpacePacket, _encoding: BufferEncoding, callback: TransformCallback): void;
22
+ }
@@ -0,0 +1,33 @@
1
+ import { Transform } from 'node:stream';
2
+ import { GapDetector } from './gapDetector.js';
3
+ /**
4
+ * A passthrough Node.js `Transform` stream that wraps {@link GapDetector}.
5
+ *
6
+ * - **Writable side**: accepts decoded {@link SpacePacket} objects.
7
+ * - **Readable side**: emits the same {@link SpacePacket} objects unchanged.
8
+ * - **Side effect**: calls `onGap` for any detected sequence count gaps.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * socket
13
+ * .pipe(new PacketAssemblerStream())
14
+ * .pipe(new GapDetectorStream({ onGap: (gap) => console.warn(gap) }))
15
+ * .on('data', (packet: SpacePacket) => process(packet));
16
+ * ```
17
+ */
18
+ export class GapDetectorStream extends Transform {
19
+ detector;
20
+ constructor(options) {
21
+ super({
22
+ readableObjectMode: true,
23
+ writableObjectMode: true,
24
+ readableHighWaterMark: options.readableHighWaterMark,
25
+ });
26
+ this.detector = new GapDetector(options);
27
+ }
28
+ _transform(packet, _encoding, callback) {
29
+ this.detector.onPacket(packet.header);
30
+ this.push(packet);
31
+ callback();
32
+ }
33
+ }
@@ -0,0 +1,12 @@
1
+ export type { PrimaryHeader, SpacePacket, PacketAssemblerOptions, PacketAssemblerStreamOptions, SequenceGap, GapDetectorOptions, GapDetectorStreamOptions, ReassembledAdu, SegmentationError, SegmentationReassemblerOptions, SegmentationReassemblerStreamOptions, } from './types.js';
2
+ export { HEADER_BYTE_LENGTH, IDLE_APID, MAX_DATA_FIELD_LENGTH, SEQUENCE_COUNT_RANGE, PacketType, SequenceFlags, } from './types.js';
3
+ export { getPacketLength, isIdlePacket } from './util.js';
4
+ export { encode } from './encode.js';
5
+ export { decode, decodeHeader } from './decode.js';
6
+ export { SpacePacketError } from './errors.js';
7
+ export { PacketAssembler } from './packetAssembler.js';
8
+ export { GapDetector } from './gapDetector.js';
9
+ export { SegmentationReassembler } from './segmentationReassembler.js';
10
+ export { PacketAssemblerStream } from './packetAssemblerStream.js';
11
+ export { GapDetectorStream } from './gapDetectorStream.js';
12
+ export { SegmentationReassemblerStream } from './segmentationReassemblerStream.js';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { HEADER_BYTE_LENGTH, IDLE_APID, MAX_DATA_FIELD_LENGTH, SEQUENCE_COUNT_RANGE, PacketType, SequenceFlags, } from './types.js';
2
+ export { getPacketLength, isIdlePacket } from './util.js';
3
+ export { encode } from './encode.js';
4
+ export { decode, decodeHeader } from './decode.js';
5
+ export { SpacePacketError } from './errors.js';
6
+ export { PacketAssembler } from './packetAssembler.js';
7
+ export { GapDetector } from './gapDetector.js';
8
+ export { SegmentationReassembler } from './segmentationReassembler.js';
9
+ export { PacketAssemblerStream } from './packetAssemblerStream.js';
10
+ export { GapDetectorStream } from './gapDetectorStream.js';
11
+ export { SegmentationReassemblerStream } from './segmentationReassemblerStream.js';
@@ -0,0 +1,26 @@
1
+ import { type PacketAssemblerOptions, type SpacePacket } from './types.js';
2
+ export declare class PacketAssembler {
3
+ private buffer;
4
+ /** Byte offset into `buffer` where the next unread packet starts. */
5
+ private readOffset;
6
+ private readonly maxBufferBytes;
7
+ get bufferedByteLength(): number;
8
+ constructor(options?: PacketAssemblerOptions);
9
+ /**
10
+ * Appends a new chunk to the internal buffer.
11
+ * `Uint8Array` is fixed-length, so this allocates a new buffer and copies both into it.
12
+ */
13
+ private append;
14
+ /**
15
+ * Discards consumed bytes from the front of the buffer and resets `readOffset` to 0.
16
+ * Call once after the decode loop ends — one compaction per `onChunk` call,
17
+ * regardless of how many packets were decoded.
18
+ */
19
+ private compact;
20
+ /**
21
+ * Feed a new chunk of bytes into the assembler.
22
+ * Returns all complete packets that could be decoded from the accumulated buffer.
23
+ */
24
+ onChunk(chunk: Uint8Array): SpacePacket[];
25
+ reset(): void;
26
+ }
@@ -0,0 +1,73 @@
1
+ import { HEADER_BYTE_LENGTH } from './types.js';
2
+ import { decodeHeader } from './decode.js';
3
+ import { getPacketLength } from './util.js';
4
+ import { SpacePacketError } from './errors.js';
5
+ export class PacketAssembler {
6
+ buffer = new Uint8Array(0);
7
+ /** Byte offset into `buffer` where the next unread packet starts. */
8
+ readOffset = 0;
9
+ maxBufferBytes;
10
+ get bufferedByteLength() {
11
+ return this.buffer.byteLength - this.readOffset;
12
+ }
13
+ constructor(options) {
14
+ this.maxBufferBytes = options?.maxBufferBytes;
15
+ }
16
+ /**
17
+ * Appends a new chunk to the internal buffer.
18
+ * `Uint8Array` is fixed-length, so this allocates a new buffer and copies both into it.
19
+ */
20
+ append(chunk) {
21
+ if (this.maxBufferBytes !== undefined &&
22
+ this.buffer.byteLength - this.readOffset + chunk.byteLength > this.maxBufferBytes) {
23
+ throw SpacePacketError.bufferOverflow();
24
+ }
25
+ const newBuffer = new Uint8Array(this.buffer.byteLength + chunk.byteLength);
26
+ newBuffer.set(this.buffer, 0);
27
+ newBuffer.set(chunk, this.buffer.byteLength);
28
+ this.buffer = newBuffer;
29
+ }
30
+ /**
31
+ * Discards consumed bytes from the front of the buffer and resets `readOffset` to 0.
32
+ * Call once after the decode loop ends — one compaction per `onChunk` call,
33
+ * regardless of how many packets were decoded.
34
+ */
35
+ compact() {
36
+ if (this.readOffset === 0)
37
+ return;
38
+ if (this.readOffset === this.buffer.byteLength) {
39
+ this.buffer = new Uint8Array(0);
40
+ }
41
+ else {
42
+ this.buffer = this.buffer.slice(this.readOffset);
43
+ }
44
+ this.readOffset = 0;
45
+ }
46
+ /**
47
+ * Feed a new chunk of bytes into the assembler.
48
+ * Returns all complete packets that could be decoded from the accumulated buffer.
49
+ */
50
+ onChunk(chunk) {
51
+ this.append(chunk);
52
+ const packets = [];
53
+ while (true) {
54
+ if (this.buffer.byteLength - this.readOffset < HEADER_BYTE_LENGTH) {
55
+ break;
56
+ }
57
+ const header = decodeHeader(this.buffer.subarray(this.readOffset));
58
+ const totalLength = getPacketLength(header);
59
+ if (this.buffer.byteLength - this.readOffset < totalLength) {
60
+ break;
61
+ }
62
+ const dataField = this.buffer.slice(this.readOffset + HEADER_BYTE_LENGTH, this.readOffset + totalLength);
63
+ packets.push({ header, dataField });
64
+ this.readOffset += totalLength;
65
+ }
66
+ this.compact();
67
+ return packets;
68
+ }
69
+ reset() {
70
+ this.buffer = new Uint8Array(0);
71
+ this.readOffset = 0;
72
+ }
73
+ }
@@ -0,0 +1,29 @@
1
+ import { Transform, type TransformCallback } from 'node:stream';
2
+ import type { PacketAssemblerStreamOptions } from './types.js';
3
+ /**
4
+ * A Node.js `Transform` stream that wraps {@link PacketAssembler}.
5
+ *
6
+ * - **Writable side**: accepts raw `Buffer` / `Uint8Array` chunks (binary mode).
7
+ * - **Readable side**: emits decoded {@link SpacePacket} objects (object mode).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { createConnection } from 'node:net';
12
+ * import { PacketAssemblerStream } from 'spacepackets';
13
+ *
14
+ * const socket = createConnection({ host: 'ground-station', port: 4321 });
15
+ * const stream = new PacketAssemblerStream({ maxBufferBytes: 1_000_000 });
16
+ *
17
+ * socket.pipe(stream);
18
+ *
19
+ * stream.on('data', (packet: SpacePacket) => {
20
+ * console.log('APID', packet.header.apid);
21
+ * });
22
+ * ```
23
+ */
24
+ export declare class PacketAssemblerStream extends Transform {
25
+ private readonly assembler;
26
+ constructor(options?: PacketAssemblerStreamOptions);
27
+ _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
28
+ _flush(callback: TransformCallback): void;
29
+ }
@@ -0,0 +1,48 @@
1
+ import { Transform } from 'node:stream';
2
+ import { PacketAssembler } from './packetAssembler.js';
3
+ import { SpacePacketError } from './errors.js';
4
+ /**
5
+ * A Node.js `Transform` stream that wraps {@link PacketAssembler}.
6
+ *
7
+ * - **Writable side**: accepts raw `Buffer` / `Uint8Array` chunks (binary mode).
8
+ * - **Readable side**: emits decoded {@link SpacePacket} objects (object mode).
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createConnection } from 'node:net';
13
+ * import { PacketAssemblerStream } from 'spacepackets';
14
+ *
15
+ * const socket = createConnection({ host: 'ground-station', port: 4321 });
16
+ * const stream = new PacketAssemblerStream({ maxBufferBytes: 1_000_000 });
17
+ *
18
+ * socket.pipe(stream);
19
+ *
20
+ * stream.on('data', (packet: SpacePacket) => {
21
+ * console.log('APID', packet.header.apid);
22
+ * });
23
+ * ```
24
+ */
25
+ export class PacketAssemblerStream extends Transform {
26
+ assembler;
27
+ constructor(options) {
28
+ super({
29
+ readableObjectMode: true,
30
+ readableHighWaterMark: options?.readableHighWaterMark,
31
+ writableObjectMode: false,
32
+ });
33
+ this.assembler = new PacketAssembler(options);
34
+ }
35
+ _transform(chunk, _encoding, callback) {
36
+ this.assembler.onChunk(chunk).forEach((packet) => this.push(packet));
37
+ callback();
38
+ }
39
+ _flush(callback) {
40
+ if (this.assembler.bufferedByteLength > 0) {
41
+ const error = SpacePacketError.incompletePacket();
42
+ callback(error);
43
+ }
44
+ else {
45
+ callback();
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,22 @@
1
+ import { type SegmentationReassemblerOptions, type SpacePacket } from './types.js';
2
+ export declare class SegmentationReassembler {
3
+ private readonly onAdu;
4
+ private readonly onError;
5
+ /** Accumulated segments per APID. Present only while an ADU is in-progress. */
6
+ private readonly segments;
7
+ constructor(options: SegmentationReassemblerOptions);
8
+ /**
9
+ * Process a single space packet.
10
+ *
11
+ * - `Standalone`: call `onAdu` immediately with the packet's data field.
12
+ * - `First`: begin accumulating segments for this APID. If an in-progress ADU already
13
+ * exists for this APID, call `onError` with `UNEXPECTED_FIRST` and abandon it.
14
+ * - `Continuation`: append to the in-progress ADU for this APID. If none exists,
15
+ * call `onError` with `ORPHANED_SEGMENT`.
16
+ * - `Last`: append and call `onAdu` with the fully concatenated data. If no in-progress
17
+ * ADU exists, call `onError` with `ORPHANED_SEGMENT`.
18
+ */
19
+ onPacket(packet: SpacePacket): void;
20
+ /** Clears all in-progress ADUs. Subsequent `Continuation`/`Last` packets will be orphaned. */
21
+ reset(): void;
22
+ }
@@ -0,0 +1,63 @@
1
+ import { SequenceFlags, } from './types.js';
2
+ import { concatUint8Arrays } from './util.js';
3
+ export class SegmentationReassembler {
4
+ onAdu;
5
+ onError;
6
+ /** Accumulated segments per APID. Present only while an ADU is in-progress. */
7
+ segments = new Map();
8
+ constructor(options) {
9
+ this.onAdu = options.onAdu;
10
+ this.onError = options.onError;
11
+ }
12
+ /**
13
+ * Process a single space packet.
14
+ *
15
+ * - `Standalone`: call `onAdu` immediately with the packet's data field.
16
+ * - `First`: begin accumulating segments for this APID. If an in-progress ADU already
17
+ * exists for this APID, call `onError` with `UNEXPECTED_FIRST` and abandon it.
18
+ * - `Continuation`: append to the in-progress ADU for this APID. If none exists,
19
+ * call `onError` with `ORPHANED_SEGMENT`.
20
+ * - `Last`: append and call `onAdu` with the fully concatenated data. If no in-progress
21
+ * ADU exists, call `onError` with `ORPHANED_SEGMENT`.
22
+ */
23
+ onPacket(packet) {
24
+ const { apid, sequenceFlags } = packet.header;
25
+ switch (sequenceFlags) {
26
+ case SequenceFlags.Standalone:
27
+ this.onAdu({ apid, data: packet.dataField });
28
+ break;
29
+ case SequenceFlags.First: {
30
+ if (this.segments.has(apid)) {
31
+ this.onError?.({ kind: 'UNEXPECTED_FIRST', apid });
32
+ this.segments.delete(apid);
33
+ }
34
+ this.segments.set(apid, [packet.dataField]);
35
+ break;
36
+ }
37
+ case SequenceFlags.Continuation: {
38
+ const parts = this.segments.get(apid);
39
+ if (!parts) {
40
+ this.onError?.({ kind: 'ORPHANED_SEGMENT', apid, sequenceFlags });
41
+ break;
42
+ }
43
+ parts.push(packet.dataField);
44
+ break;
45
+ }
46
+ case SequenceFlags.Last: {
47
+ const parts = this.segments.get(apid);
48
+ if (!parts) {
49
+ this.onError?.({ kind: 'ORPHANED_SEGMENT', apid, sequenceFlags });
50
+ break;
51
+ }
52
+ parts.push(packet.dataField);
53
+ this.segments.delete(apid);
54
+ this.onAdu({ apid, data: concatUint8Arrays(parts) });
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ /** Clears all in-progress ADUs. Subsequent `Continuation`/`Last` packets will be orphaned. */
60
+ reset() {
61
+ this.segments.clear();
62
+ }
63
+ }
@@ -0,0 +1,25 @@
1
+ import { Transform, type TransformCallback } from 'node:stream';
2
+ import type { SegmentationReassemblerStreamOptions, SpacePacket } from './types.js';
3
+ /**
4
+ * A Node.js `Transform` stream that wraps {@link SegmentationReassembler}.
5
+ *
6
+ * - **Writable side**: accepts decoded {@link SpacePacket} objects.
7
+ * - **Readable side**: emits {@link ReassembledAdu} objects — one per complete `First`→`Last` sequence
8
+ * or `Standalone` packet.
9
+ *
10
+ * `onError` is optional — orphaned or unexpected segments are silently ignored if not provided.
11
+ * In-progress ADUs that are incomplete when the stream ends are silently abandoned.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * socket
16
+ * .pipe(new PacketAssemblerStream())
17
+ * .pipe(new SegmentationReassemblerStream({ onError: (err) => console.warn(err) }))
18
+ * .on('data', (adu: ReassembledAdu) => process(adu));
19
+ * ```
20
+ */
21
+ export declare class SegmentationReassemblerStream extends Transform {
22
+ private readonly reassembler;
23
+ constructor(options?: SegmentationReassemblerStreamOptions);
24
+ _transform(packet: SpacePacket, _encoding: BufferEncoding, callback: TransformCallback): void;
25
+ }
@@ -0,0 +1,38 @@
1
+ import { Transform } from 'node:stream';
2
+ import { SegmentationReassembler } from './segmentationReassembler.js';
3
+ /**
4
+ * A Node.js `Transform` stream that wraps {@link SegmentationReassembler}.
5
+ *
6
+ * - **Writable side**: accepts decoded {@link SpacePacket} objects.
7
+ * - **Readable side**: emits {@link ReassembledAdu} objects — one per complete `First`→`Last` sequence
8
+ * or `Standalone` packet.
9
+ *
10
+ * `onError` is optional — orphaned or unexpected segments are silently ignored if not provided.
11
+ * In-progress ADUs that are incomplete when the stream ends are silently abandoned.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * socket
16
+ * .pipe(new PacketAssemblerStream())
17
+ * .pipe(new SegmentationReassemblerStream({ onError: (err) => console.warn(err) }))
18
+ * .on('data', (adu: ReassembledAdu) => process(adu));
19
+ * ```
20
+ */
21
+ export class SegmentationReassemblerStream extends Transform {
22
+ reassembler;
23
+ constructor(options) {
24
+ super({
25
+ readableObjectMode: true,
26
+ writableObjectMode: true,
27
+ readableHighWaterMark: options?.readableHighWaterMark,
28
+ });
29
+ this.reassembler = new SegmentationReassembler({
30
+ onAdu: (adu) => this.push(adu),
31
+ ...(options?.onError !== undefined ? { onError: options.onError } : {}),
32
+ });
33
+ }
34
+ _transform(packet, _encoding, callback) {
35
+ this.reassembler.onPacket(packet);
36
+ callback();
37
+ }
38
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Constants and types for CCSDS space packets.
3
+ /
4
+
5
+ /*
6
+ * The main types are `PrimaryHeader` (the fixed 6-byte header present in every packet)
7
+ * and `SpacePacket` (the full decoded packet, including header and data field).
8
+ */
9
+ export declare const HEADER_BYTE_LENGTH = 6;
10
+ /**
11
+ * The maximum number of bytes in a packet data field (CCSDS 133.0-B-2 §4.1.3.5.3).
12
+ * Total maximum packet size is `HEADER_BYTE_LENGTH + MAX_DATA_FIELD_LENGTH` = 65542 bytes.
13
+ */
14
+ export declare const MAX_DATA_FIELD_LENGTH = 65536;
15
+ /**
16
+ * The number of distinct sequence count values (2¹⁴ = 16384, for values 0–16383).
17
+ * After reaching 16383, the sequence count wraps back to 0.
18
+ * Use this as the modulus when computing expected counts or gap sizes.
19
+ */
20
+ export declare const SEQUENCE_COUNT_RANGE = 16384;
21
+ /**
22
+ * The reserved APID for idle (fill) packets. Packets with this APID carry no
23
+ * mission data and should be discarded by ground station routing logic.
24
+ */
25
+ export declare const IDLE_APID = 2047;
26
+ /** Whether the packet carries telemetry (spacecraft → ground) or a telecommand (ground → spacecraft). */
27
+ export declare enum PacketType {
28
+ /**
29
+ * Spacecraft → ground. The most common type of packet, used for sending measurements, status reports, and other data from the spacecraft to the ground.
30
+ */
31
+ Telemetry = 0,
32
+ /**
33
+ * Ground → spacecraft. Used for sending commands and instructions from the ground to the spacecraft.
34
+ */
35
+ Telecommand = 1
36
+ }
37
+ /**
38
+ * Indicates whether this packet is a standalone unit or a segment of a larger application unit.
39
+ * - `Standalone` — a complete, self-contained packet (most common for telemetry)
40
+ * - `First` / `Continuation` / `Last` — segments of a larger unit that must be reassembled
41
+ */
42
+ export declare enum SequenceFlags {
43
+ Continuation = 0,
44
+ First = 1,
45
+ Last = 2,
46
+ Standalone = 3
47
+ }
48
+ /**
49
+ * The mandatory 6-byte CCSDS primary header present in every space packet.
50
+ *
51
+ * Bit layout (big-endian):
52
+ * ```
53
+ * Byte 0: [PVN(3)] [Type(1)] [SecondaryHeaderFlag(1)] [APID_hi(3)]
54
+ * Byte 1: [APID_lo(8)]
55
+ * Byte 2: [SequenceFlags(2)] [SequenceCount_hi(6)]
56
+ * Byte 3: [SequenceCount_lo(8)]
57
+ * Bytes 4–5: [DataLength(16)]
58
+ * ```
59
+ */
60
+ export interface PrimaryHeader {
61
+ /**
62
+ * Packet version number. Must be `0` for valid CCSDS packets; if you see a different value, the packet is malformed or uses an unsupported version of the standard. (The CCSDS standard reserves non-zero values for future versions, but as of this writing only version 0 exists.)
63
+ */
64
+ readonly packetVersionNumber: 0;
65
+ readonly packetType: PacketType;
66
+ /**
67
+ * Indicates whether a secondary header is present. The format of the secondary header is mission-specific and not parsed by this library; callers who need it can slice `dataField` themselves.
68
+ */
69
+ readonly secondaryHeaderFlag: boolean;
70
+ /**
71
+ * Application Process ID. A mission-specific identifier for the source or destination of the packet, used for routing and demultiplexing. Valid values are 0–2047 (11 bits); if you see a larger value, the packet is malformed.
72
+ */
73
+ readonly apid: number;
74
+ /**
75
+ * Indicates whether this packet is a complete standalone unit or a segment of a larger application data unit.
76
+ * Most telemetry packets are `SequenceFlags.Standalone`. Use `First`/`Continuation`/`Last` for segmented transfers.
77
+ */
78
+ readonly sequenceFlags: SequenceFlags;
79
+ /**
80
+ * Sequence count, used for identifying segments of larger application units that span multiple packets. For standalone packets, `sequenceFlags` is `SequenceFlags.Standalone` and `sequenceCount` is typically 0, but these fields can be set to other values for mission-specific purposes if desired. Valid `sequenceCount` values are 0–16383 (14 bits); if you see a larger value, the packet is malformed.
81
+ */
82
+ readonly sequenceCount: number;
83
+ /**
84
+ * Data length, defined as the number of bytes in the data field minus one. This is a quirk of the CCSDS standard; if you see a value that doesn't match `dataField.byteLength - 1`, the packet is malformed.
85
+ */
86
+ readonly dataLength: number;
87
+ }
88
+ /**
89
+ * A decoded CCSDS space packet.
90
+ *
91
+ * `dataField` is the complete data field — secondary header (if present) plus user data —
92
+ * as a single opaque byte array. The secondary header format is mission-specific and not
93
+ * parsed by this library; callers who need it can slice `dataField` themselves.
94
+ *
95
+ * The relationship between `header.dataLength` and `dataField` is always:
96
+ * `dataField.byteLength === header.dataLength + 1`
97
+ */
98
+ /**
99
+ * Constructor options for {@link PacketAssembler}.
100
+ */
101
+ export interface PacketAssemblerOptions {
102
+ /**
103
+ * Maximum number of bytes the internal buffer may hold at any one time.
104
+ * If a chunk would cause the buffer to exceed this limit, `onChunk` should throw
105
+ * a `SpacePacketError` with code `BUFFER_OVERFLOW`.
106
+ * Defaults to no limit.
107
+ */
108
+ maxBufferBytes?: number;
109
+ }
110
+ /**
111
+ * Constructor options for {@link PacketAssemblerStream}.
112
+ * Extends {@link PacketAssemblerOptions} with any Node.js `Transform` stream options.
113
+ */
114
+ export interface PacketAssemblerStreamOptions extends PacketAssemblerOptions {
115
+ /**
116
+ * High-water mark for the readable side of the stream (number of objects).
117
+ * Defaults to `16`.
118
+ */
119
+ readableHighWaterMark?: number;
120
+ }
121
+ /**
122
+ * Describes a detected gap in the sequence count for a given APID.
123
+ */
124
+ export interface SequenceGap {
125
+ /** The APID where the gap was detected. */
126
+ readonly apid: number;
127
+ /** The sequence count that was expected but not received. */
128
+ readonly expected: number;
129
+ /** The sequence count that was actually received. */
130
+ readonly received: number;
131
+ /** The number of missing packets. Accounts for wrap-around at 16383 → 0. */
132
+ readonly missing: number;
133
+ }
134
+ /**
135
+ * Constructor options for {@link GapDetector}.
136
+ */
137
+ export interface GapDetectorOptions {
138
+ /** Called once for each gap detected, with details about the missing range. */
139
+ onGap: (gap: SequenceGap) => void;
140
+ }
141
+ /**
142
+ * Constructor options for {@link GapDetectorStream}.
143
+ * Extends {@link GapDetectorOptions} with any Node.js `Transform` stream options.
144
+ */
145
+ export interface GapDetectorStreamOptions extends GapDetectorOptions {
146
+ /**
147
+ * High-water mark for the readable side of the stream (number of objects).
148
+ * Defaults to `16`.
149
+ */
150
+ readableHighWaterMark?: number;
151
+ }
152
+ /**
153
+ * A fully reassembled application data unit (ADU) produced by {@link SegmentationReassembler}.
154
+ * Contains the concatenated data fields from a `First` → `Continuation`* → `Last` sequence,
155
+ * or the data field of a single `Standalone` packet.
156
+ */
157
+ export interface ReassembledAdu {
158
+ readonly apid: number;
159
+ readonly data: Uint8Array;
160
+ }
161
+ /**
162
+ * Emitted by {@link SegmentationReassembler} when a segment arrives out of order or
163
+ * without a matching counterpart.
164
+ *
165
+ * - `ORPHANED_SEGMENT` — a `Continuation` or `Last` arrived with no active `First` for this APID.
166
+ * - `UNEXPECTED_FIRST` — a `First` arrived while an in-progress ADU already exists for this APID;
167
+ * the previous ADU is abandoned.
168
+ */
169
+ export type SegmentationError = {
170
+ readonly kind: 'ORPHANED_SEGMENT';
171
+ readonly apid: number;
172
+ readonly sequenceFlags: SequenceFlags.Continuation | SequenceFlags.Last;
173
+ } | {
174
+ readonly kind: 'UNEXPECTED_FIRST';
175
+ readonly apid: number;
176
+ };
177
+ /**
178
+ * Constructor options for {@link SegmentationReassembler}.
179
+ */
180
+ export interface SegmentationReassemblerOptions {
181
+ /** Called when a complete ADU has been reassembled. */
182
+ onAdu: (adu: ReassembledAdu) => void;
183
+ /** Called when a segment arrives that cannot be reassembled. Optional — errors are silently ignored if not provided. */
184
+ onError?: (error: SegmentationError) => void;
185
+ }
186
+ /**
187
+ * Constructor options for {@link SegmentationReassemblerStream}.
188
+ * `onAdu` is omitted — the stream emits reassembled ADUs via `push()` internally.
189
+ */
190
+ export interface SegmentationReassemblerStreamOptions extends Omit<SegmentationReassemblerOptions, 'onAdu'> {
191
+ /**
192
+ * High-water mark for the readable side of the stream (number of objects).
193
+ * Defaults to `16`.
194
+ */
195
+ readableHighWaterMark?: number;
196
+ }
197
+ export interface SpacePacket {
198
+ readonly header: PrimaryHeader;
199
+ readonly dataField: Uint8Array;
200
+ }
package/dist/types.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Constants and types for CCSDS space packets.
3
+ /
4
+
5
+ /*
6
+ * The main types are `PrimaryHeader` (the fixed 6-byte header present in every packet)
7
+ * and `SpacePacket` (the full decoded packet, including header and data field).
8
+ */
9
+ export const HEADER_BYTE_LENGTH = 6;
10
+ /**
11
+ * The maximum number of bytes in a packet data field (CCSDS 133.0-B-2 §4.1.3.5.3).
12
+ * Total maximum packet size is `HEADER_BYTE_LENGTH + MAX_DATA_FIELD_LENGTH` = 65542 bytes.
13
+ */
14
+ export const MAX_DATA_FIELD_LENGTH = 65536;
15
+ /**
16
+ * The number of distinct sequence count values (2¹⁴ = 16384, for values 0–16383).
17
+ * After reaching 16383, the sequence count wraps back to 0.
18
+ * Use this as the modulus when computing expected counts or gap sizes.
19
+ */
20
+ export const SEQUENCE_COUNT_RANGE = 16384;
21
+ /**
22
+ * The reserved APID for idle (fill) packets. Packets with this APID carry no
23
+ * mission data and should be discarded by ground station routing logic.
24
+ */
25
+ export const IDLE_APID = 0x7ff;
26
+ /** Whether the packet carries telemetry (spacecraft → ground) or a telecommand (ground → spacecraft). */
27
+ export var PacketType;
28
+ (function (PacketType) {
29
+ /**
30
+ * Spacecraft → ground. The most common type of packet, used for sending measurements, status reports, and other data from the spacecraft to the ground.
31
+ */
32
+ PacketType[PacketType["Telemetry"] = 0] = "Telemetry";
33
+ /**
34
+ * Ground → spacecraft. Used for sending commands and instructions from the ground to the spacecraft.
35
+ */
36
+ PacketType[PacketType["Telecommand"] = 1] = "Telecommand";
37
+ })(PacketType || (PacketType = {}));
38
+ /**
39
+ * Indicates whether this packet is a standalone unit or a segment of a larger application unit.
40
+ * - `Standalone` — a complete, self-contained packet (most common for telemetry)
41
+ * - `First` / `Continuation` / `Last` — segments of a larger unit that must be reassembled
42
+ */
43
+ export var SequenceFlags;
44
+ (function (SequenceFlags) {
45
+ SequenceFlags[SequenceFlags["Continuation"] = 0] = "Continuation";
46
+ SequenceFlags[SequenceFlags["First"] = 1] = "First";
47
+ SequenceFlags[SequenceFlags["Last"] = 2] = "Last";
48
+ SequenceFlags[SequenceFlags["Standalone"] = 3] = "Standalone";
49
+ })(SequenceFlags || (SequenceFlags = {}));
package/dist/util.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { type PrimaryHeader } from './types.js';
2
+ export declare function getPacketLength(header: PrimaryHeader): number;
3
+ /**
4
+ * Returns `true` if the packet's APID is the reserved idle/fill APID (`0x7FF`).
5
+ * Idle packets carry no mission data and should be discarded before routing.
6
+ */
7
+ export declare function isIdlePacket(header: PrimaryHeader): boolean;
8
+ /** Concatenates an array of `Uint8Array` chunks into a single contiguous buffer. */
9
+ export declare function concatUint8Arrays(parts: Uint8Array[]): Uint8Array;
package/dist/util.js ADDED
@@ -0,0 +1,22 @@
1
+ import { HEADER_BYTE_LENGTH, IDLE_APID } from './types.js';
2
+ export function getPacketLength(header) {
3
+ return HEADER_BYTE_LENGTH + header.dataLength + 1;
4
+ }
5
+ /**
6
+ * Returns `true` if the packet's APID is the reserved idle/fill APID (`0x7FF`).
7
+ * Idle packets carry no mission data and should be discarded before routing.
8
+ */
9
+ export function isIdlePacket(header) {
10
+ return header.apid === IDLE_APID;
11
+ }
12
+ /** Concatenates an array of `Uint8Array` chunks into a single contiguous buffer. */
13
+ export function concatUint8Arrays(parts) {
14
+ const totalLength = parts.reduce((sum, p) => sum + p.byteLength, 0);
15
+ const result = new Uint8Array(totalLength);
16
+ let offset = 0;
17
+ for (const part of parts) {
18
+ result.set(part, offset);
19
+ offset += part.byteLength;
20
+ }
21
+ return result;
22
+ }
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@onion-party/spacepackets",
3
+ "version": "1.0.0",
4
+ "description": "Encode and decode CCSDS Space Packets for ground station and spacecraft software.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE",
19
+ "logo.png"
20
+ ],
21
+ "scripts": {
22
+ "clean": "rm -rf dist",
23
+ "build": "tsc --project tsconfig.build.json",
24
+ "dev": "tsc --project tsconfig.build.json --watch",
25
+ "test": "vitest",
26
+ "test:run": "vitest run",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "oxlint --config oxlint.json src/",
29
+ "format": "oxfmt src/",
30
+ "format:check": "oxfmt --check src/",
31
+ "coverage": "vitest run --coverage",
32
+ "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test:run",
33
+ "prepublishOnly": "pnpm run check && pnpm run build",
34
+ "prepare": "husky"
35
+ },
36
+ "keywords": [
37
+ "ccsds",
38
+ "space-packet",
39
+ "telemetry",
40
+ "telecommand",
41
+ "ground-station",
42
+ "space"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "sideEffects": false,
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "author": "ryangibbs",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/ryangibbs/spacepackets.git"
56
+ },
57
+ "homepage": "https://github.com/ryangibbs/spacepackets#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/ryangibbs/spacepackets/issues"
60
+ },
61
+ "devDependencies": {
62
+ "@commitlint/cli": "^20.5.0",
63
+ "@commitlint/config-conventional": "^20.5.0",
64
+ "@semantic-release/changelog": "^6.0.3",
65
+ "@semantic-release/git": "^10.0.1",
66
+ "@types/node": "^22.0.0",
67
+ "@vitest/coverage-v8": "^3.2.4",
68
+ "husky": "^9.1.7",
69
+ "lint-staged": "^16.4.0",
70
+ "oxfmt": "^0.44.0",
71
+ "oxlint": "^1.59.0",
72
+ "semantic-release": "^25.0.3",
73
+ "typescript": "^5.8.0",
74
+ "vitest": "^3.1.0"
75
+ },
76
+ "lint-staged": {
77
+ "src/**/*.ts": [
78
+ "oxlint --config oxlint.json",
79
+ "oxfmt"
80
+ ]
81
+ },
82
+ "packageManager": "pnpm@10.33.0"
83
+ }