bkjbbs
v1.2.1
Published
Packet radio BBS library for Node.js.
Downloads
831
Readme
bkjbbs
Packet radio BBS, KISS, and AX.25 library for Node.js.
Installation
npm install bkjbbsTCP KISS works without additional dependencies. For a serial TNC, install the
optional serialport peer dependency:
npm install serialportbkjbbs is an ES module and includes TypeScript declarations.
Quick start
import { BBS } from "bkjbbs";
const bbs = new BBS({
kiss: {
tcp: {
host: "localhost",
port: 8001,
},
},
bbs: {
callsign: process.env.BBS_CALLSIGN ?? "N0CALL",
},
async onSession(session) {
await session.sendLine("Hello from my BBS!");
const line = await session.readLine();
await session.sendLine(`You wrote: ${line}`);
await session.disconnect();
},
});
await bbs.start();Serial KISS TNCs use kiss.serial. The default host-to-TNC speed is 115200
baud:
const bbs = new BBS({
kiss: {
serial: {
port: "/dev/ttyUSB0",
baudRate: 115200,
},
},
bbs: {
callsign: "N0CALL",
},
});
await bbs.start();Exports
The package root exports:
import {
BBS,
BBSSession,
normalizeConfig,
parseCallsign,
formatCallsign,
encodeAddress,
decodeAddress,
encodeFrame,
encodeUIFrame,
encodeIFrame,
encodeSupervisoryFrame,
encodeUnnumberedFrame,
decodeFrame,
CONTROL_UI,
CONTROL_DM,
CONTROL_SABM,
CONTROL_DISC,
CONTROL_UA,
PID_NO_LAYER_3,
kiss,
} from "bkjbbs";BBS and BBSSession provide the BBS/session API. The other root exports are
low-level AX.25 helpers. KISS framing is grouped under the kiss namespace.
BBS API
new BBS(config, options?)
Creates a BBS instance. Configuration is validated and normalized immediately. Exactly one KISS transport must be configured.
new BBS(config: BBSConfig, options?: BBSConstructorOptions)The optional second argument is mainly useful for custom transports and tests:
interface BBSConstructorOptions {
transport?: Transport;
setTimer?: typeof setTimeout;
clearTimer?: typeof clearTimeout;
}Configuration
interface BBSConfig {
kiss: {
tcp?: KissTcpConfig;
serial?: KissSerialConfig;
};
bbs: BBSOptions;
log?: LogOptions;
onSession?: (session: BBSSession) => void | Promise<void>;
onSessionError?: (error: Error, session: BBSSession) => void;
onSessionClose?: (session: BBSSession, reason: SessionCloseReason) => void;
onPacket?: (packet: kiss.Packet) => void | Promise<void>;
onTransmitPacket?: (packet: kiss.Packet) => void | Promise<void>;
onDecodeError?: (error: Error, packet: kiss.Packet) => void | Promise<void>;
onFrame?: (frame: Ax25Frame) => void | Promise<void>;
onTransmitFrame?: (
frame: Ax25Frame,
packet: kiss.Packet,
) => void | Promise<void>;
onUI?: (frame: Ax25Frame) => void | Promise<void>;
}TCP transport configuration:
| Property | Type | Description |
| -------- | -------- | ------------------------------------ |
| host | string | KISS TCP server hostname or address. |
| port | number | KISS TCP server port. |
Serial transport configuration:
| Property | Type | Default | Description |
| ---------- | --------------------------- | -------- | ---------------------- |
| port | string | required | Serial device path. |
| baudRate | number | 115200 | Host-to-TNC baud rate. |
| parity | "none" \| "even" \| "odd" | "none" | Serial parity. |
| stopBits | 1 \| 2 | 1 | Number of stop bits. |
| dataBits | 5 \| 6 \| 7 \| 8 | 8 | Number of data bits. |
BBS options:
| Property | Type | Default | Description |
| --------------------- | -------- | -------- | --------------------------------------------------------------------- |
| callsign | string | required | Local AX.25 callsign. It is validated, uppercased, and normalized. |
| lineEnding | string | "\r" | Text appended by session.sendLine(). |
| retransmitTimeoutMs | number | 15000 | Delay before connected-mode acknowledgement recovery starts. |
| maxRetries | number | 3 | Maximum connected-mode recovery/retransmission attempts. |
| sendWindow | number | 7 | Maximum unacknowledged I frames. Must be an integer from 1 through 7. |
Logging options:
| Property | Default | Description |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| logPackets | false | Writes received and transmitted AX.25 frame summaries as timestamped, single-line JSON to console.log. |
| logConnections | false | Reserved configuration option; currently does not emit logs. |
| logMessages | false | Reserved configuration option; currently does not emit logs. |
Event handlers
| Handler | When it runs |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| onSession(session) | After an incoming SABM is accepted and its UA response is sent. Each peer/port pair has an independent session. |
| onSessionError(error, session) | When onSession or connected-mode timer handling fails. Without this handler, the error is written to console.error. |
| onSessionClose(session, reason) | When a protocol session closes, locally disconnects, reconnects, or reaches its retry limit. |
| onPacket(packet) | For every received KISS packet, before KISS command filtering or AX.25 decoding. |
| onTransmitPacket(packet) | Before an outbound KISS packet is passed to the transport. |
| onDecodeError(error, packet) | When a KISS data packet cannot be decoded as AX.25. If present, it handles the error. |
| onFrame(frame) | For every successfully decoded received AX.25 data frame, before BBS/UI handling. |
| onTransmitFrame(frame, packet) | Before every outbound AX.25 frame is passed to the transport. |
| onUI(frame) | For every successfully decoded received UI frame. |
Received packets are processed in order. Promise-returning packet/frame handlers
are awaited. onSession runs as a separate asynchronous session handler after
the connection has been accepted.
Properties
| Property | Type | Description |
| --------- | ----------- | --------------------------------------------- |
| config | BBSConfig | Normalized configuration, including defaults. |
| started | boolean | Whether the transport has been started. |
bbs.start()
start(): Promise<void>Registers the packet listener and connects the configured KISS transport. Calling it again while started has no effect.
bbs.stop()
stop(): Promise<void>Removes the packet listener, closes the transport, closes active sessions, and
sets started to false. Calling it while stopped has no effect.
bbs.sendUI(frame)
interface SendUIFrame {
destination: string;
source?: string;
digipeaters?: string[];
pid?: number;
port?: number;
info?: Buffer | Uint8Array | string;
}
sendUI(frame: SendUIFrame): Promise<void>Encodes and sends an AX.25 UI frame in a KISS data packet. source defaults to
the configured BBS callsign, pid defaults to PID_NO_LAYER_3 (0xf0), and
port defaults to KISS port 0. The BBS must be started first.
Session API
BBSSession instances are created by BBS for accepted connected-mode peers
and are supplied to onSession. Applications normally do not construct them
directly.
Properties
| Property | Type | Description |
| --------------- | --------------------------------- | ------------------------------------------------ |
| peerCallsign | string | Normalized remote callsign. |
| localCallsign | string | Normalized local BBS callsign. |
| port | number | KISS port carrying the connection. |
| lineEnding | string | Outgoing line ending configured on the BBS. |
| closed | boolean | Whether the session is closed. |
| closeReason | SessionCloseReason \| undefined | Reason recorded when the BBS closes the session. |
Known close reasons are "closed", "local-disconnect", "reconnected", and
"retry-limit". The type also permits transport/application-defined strings.
session.send(text)
send(text: unknown): Promise<void>Converts text with String(text), encodes it as UTF-8, and sends it as
connected-mode data without adding a line ending.
session.sendLine(text)
sendLine(text: unknown): Promise<void>Sends String(text) followed by the configured lineEnding.
session.sendRaw(buffer)
sendRaw(buffer: Buffer | Uint8Array): Promise<void>Sends bytes as connected-mode data. Session sends are serialized and observe the configured AX.25 send window. The promise can wait for acknowledgements when the window is full and rejects if the session closes, starts disconnecting, or the peer reports receive-not-ready.
session.readLine()
readLine(): Promise<string>Returns the next UTF-8 line without its terminator. Incoming CR, LF, and
CRLF are recognized. Complete lines received before the call are buffered.
session.waitForInput()
waitForInput(): Promise<Buffer>Returns the next received I-frame information field as a Buffer. Input
received before the call is buffered. This raw-input queue is independent of
the text line buffer.
session.onMessage(callback)
onMessage(callback: (data: Buffer) => void): () => voidRegisters a synchronous callback for every received I-frame information field. It returns a function that removes the listener. Existing buffered input is not replayed.
session.disconnect()
disconnect(): Promise<void>Starts a graceful local disconnect. Outstanding I frames are allowed to be acknowledged before DISC is sent. The session closes after the peer responds with UA or DM. Calling it on an already closed session has no effect.
Pending readLine(), waitForInput(), and blocked send operations reject with
an Error whose message is BBS session is closed when the session closes.
AX.25 callsigns and addresses
parseCallsign(value)
interface Callsign {
base: string;
ssid: number;
text: string;
}
parseCallsign(value: string): CallsignTrims and uppercases a callsign, validates a 1-6 character alphanumeric base
and optional SSID from 0 through 15, and returns its components. text omits
the -0 suffix.
parseCallsign(" sp6bkj-3 ");
// { base: "SP6BKJ", ssid: 3, text: "SP6BKJ-3" }formatCallsign(callsign)
formatCallsign(callsign: { base: string; ssid: number }): stringUppercases the base and formats the SSID. SSID 0 is omitted. This helper
formats an object; use parseCallsign() when validation is required.
encodeAddress(value, options?)
interface EncodeAddressOptions {
commandResponse?: boolean;
last?: boolean;
}
encodeAddress(
value: string | { base: string; ssid: number },
options?: EncodeAddressOptions,
): BufferEncodes one seven-byte AX.25 address field. commandResponse controls the C
bit and last controls the address-extension bit. String input is validated by
parseCallsign().
decodeAddress(buffer, offset?)
interface DecodedAddress extends Callsign {
commandResponse: boolean;
last: boolean;
nextOffset: number;
}
decodeAddress(
buffer: Buffer | Uint8Array,
offset?: number,
): DecodedAddressDecodes one AX.25 address field at offset, which defaults to 0.
nextOffset points immediately after the decoded seven-byte field. Invalid
address characters and truncated fields throw.
AX.25 frames
Frame input
type Ax25FrameType =
| "UI"
| "I"
| "RR"
| "RNR"
| "REJ"
| "SABM"
| "UA"
| "DISC"
| "DM"
| "S"
| "UNKNOWN";
type Ax25CommandResponse = "command" | "response" | "previous-version";
interface EncodeAx25Frame {
type: Ax25FrameType;
destination: string;
source: string;
digipeaters?: string[];
commandResponse?: Ax25CommandResponse;
pollFinal?: boolean;
pid?: number;
sendSequence?: number;
receiveSequence?: number;
info?: Buffer | Uint8Array | string;
}Only the encodable types accepted by the specific encoder are valid. Modulo-8
sendSequence and receiveSequence values must be integers from 0 through 7.
Decoded frame
interface Ax25Frame {
type: Ax25FrameType;
destination: string;
source: string;
digipeaters: string[];
commandResponse: Ax25CommandResponse;
control: number;
pollFinal: boolean;
pid?: number;
sendSequence?: number;
receiveSequence?: number;
info: Buffer;
raw: Buffer;
}raw contains the complete undecorated AX.25 frame. pid is present on UI and
I frames. Sequence properties are present on the frame types that carry them.
encodeFrame(frame)
encodeFrame(frame: EncodeAx25Frame): BufferDispatches to the matching UI, I, supervisory, or unnumbered encoder. Supported
encoded types are UI, I, RR, RNR, REJ, SABM, UA, DISC, and
DM. Unsupported types throw.
encodeUIFrame(frame)
encodeUIFrame(frame: Omit<EncodeAx25Frame, "type">): BufferEncodes a UI frame. digipeaters defaults to an empty list, info defaults to
empty, and pid defaults to PID_NO_LAYER_3.
encodeIFrame(frame)
encodeIFrame(
frame: EncodeAx25Frame & { type?: "I" },
): BufferEncodes a modulo-8 connected-mode I frame. Sequence numbers default to 0,
commandResponse defaults to "command", info defaults to empty, and pid
defaults to PID_NO_LAYER_3.
encodeSupervisoryFrame(frame)
encodeSupervisoryFrame(
frame: EncodeAx25Frame & {
type: "RR" | "RNR" | "REJ";
},
): BufferEncodes an RR, RNR, or REJ frame. receiveSequence defaults to 0 and
commandResponse defaults to "response".
encodeUnnumberedFrame(frame)
encodeUnnumberedFrame(
frame: EncodeAx25Frame & {
type: "SABM" | "UA" | "DISC" | "DM";
},
): BufferEncodes a connected-mode unnumbered control frame. SABM and DISC default to
"command"; UA and DM default to "response".
decodeFrame(buffer)
decodeFrame(buffer: Buffer | Uint8Array): Ax25FrameDecodes an AX.25 frame, including destination, source, digipeaters, C bits, P/F bit, sequence numbers, PID, information bytes, and raw bytes. Malformed or truncated frames throw.
AX.25 constants
| Constant | Value |
| ---------------- | ------ |
| CONTROL_UI | 0x03 |
| CONTROL_DM | 0x0f |
| CONTROL_SABM | 0x2f |
| CONTROL_DISC | 0x43 |
| CONTROL_UA | 0x63 |
| PID_NO_LAYER_3 | 0xf0 |
KISS API
All KISS exports are properties of the kiss namespace:
import { kiss } from "bkjbbs";KISS packet
interface kiss.Packet {
port: number;
command: number;
payload: Buffer;
}KISS ports and commands are four-bit integers from 0 through 15.
kiss.encodeFrame(payload, options?)
interface kiss.EncodeFrameOptions {
port?: number;
command?: number;
}
kiss.encodeFrame(
payload: Buffer | Uint8Array | string,
options?: kiss.EncodeFrameOptions,
): BufferEncodes and escapes one complete KISS frame. port defaults to 0 and
command defaults to kiss.COMMAND_DATA.
kiss.decodeFrames(buffer)
interface kiss.DecodeFramesResult {
frames: kiss.Packet[];
remainder: Buffer;
}
kiss.decodeFrames(
buffer: Buffer | Uint8Array,
): kiss.DecodeFramesResultDecodes all complete KISS frames in one buffer. remainder contains bytes from
an incomplete trailing frame. For input split across multiple chunks, use
kiss.Decoder instead.
new kiss.Decoder()
A stateful streaming KISS decoder.
const decoder = new kiss.Decoder();
decoder.write(firstChunk); // complete packets available so far
decoder.write(secondChunk);
decoder.remainder; // current incomplete frame bytesdecoder.write(buffer)
write(buffer: Buffer | Uint8Array): kiss.Packet[]Consumes a chunk and returns every packet completed by that chunk. Bytes before the first KISS frame delimiter are ignored.
decoder.remainder
readonly remainder: BufferReturns a copy of the currently buffered incomplete frame bytes.
KISS constants
| Constant | Value | Meaning |
| ------------------- | ------ | ------------------------ |
| kiss.COMMAND_DATA | 0x00 | KISS data-frame command. |
| kiss.FEND | 0xc0 | Frame delimiter. |
| kiss.FESC | 0xdb | Escape byte. |
| kiss.TFEND | 0xdc | Escaped frame delimiter. |
| kiss.TFESC | 0xdd | Escaped escape byte. |
Custom transport
A custom transport can be supplied through the second BBS constructor
argument:
interface Transport {
onPacket(callback: (packet: kiss.Packet) => void | Promise<void>): () => void;
connect(): Promise<void>;
close(): Promise<void>;
sendPacket(packet: kiss.Packet): Promise<void>;
}const bbs = new BBS(config, {
transport: myTransport,
});onPacket() must return a listener-removal function. The BBS calls connect()
from start(), close() from stop(), and sendPacket() for outbound KISS
packets.
Configuration helper
normalizeConfig(config)
normalizeConfig(config: BBSConfig): BBSConfigValidates the transport selection, required callsign, callsign syntax, and send
window. It returns a new top-level configuration object with the normalized
callsign and all BBS/logging defaults applied. BBS calls this automatically.
TypeScript utility types
The declarations also export:
type MaybePromise<T> = T | Promise<T>;
type RemoveListener = () => void;
type SessionCloseReason =
| "closed"
| "local-disconnect"
| "reconnected"
| "retry-limit"
| string;The named interfaces shown throughout this README are exported as well:
Callsign, DecodedAddress, EncodeAddressOptions, Ax25Frame,
EncodeAx25Frame, SendUIFrame, KissTcpConfig, KissSerialConfig,
BBSOptions, LogOptions, BBSConfig, Transport, and
BBSConstructorOptions.
See the changelog for release notes.
