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

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 bkjbbs

TCP KISS works without additional dependencies. For a serial TNC, install the optional serialport peer dependency:

npm install serialport

bkjbbs 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): () => void

Registers 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): Callsign

Trims 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 }): string

Uppercases 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,
): Buffer

Encodes 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,
): DecodedAddress

Decodes 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): Buffer

Dispatches 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">): Buffer

Encodes 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" },
): Buffer

Encodes 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";
  },
): Buffer

Encodes 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";
  },
): Buffer

Encodes a connected-mode unnumbered control frame. SABM and DISC default to "command"; UA and DM default to "response".

decodeFrame(buffer)

decodeFrame(buffer: Buffer | Uint8Array): Ax25Frame

Decodes 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,
): Buffer

Encodes 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.DecodeFramesResult

Decodes 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 bytes

decoder.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: Buffer

Returns 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): BBSConfig

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