Skip to content

Repository files navigation

BleKissTncNimble

Header-only Arduino library for ESP32 + NimBLE-Arduino implementing the BLE-KISS API.

The library is split into a standalone KISS codec/parser, an Arduino Stream compatible BLE-KISS GATT byte stream, and a transport-agnostic KissStreamTnc.

Spec and UUIDs

BLE-KISS API spec:

UUIDs implemented by default:

  • Service: 00000001-ba2a-46c9-ae49-01b0961f68bb
  • TX characteristic (app -> TNC, write): 00000002-ba2a-46c9-ae49-01b0961f68bb
  • RX characteristic (TNC -> app, notify/read): 00000003-ba2a-46c9-ae49-01b0961f68bb

Features

  • Header-only BleKissGattStream raw BLE byte-stream template implementing Arduino Stream
  • Header-only KissStreamTnc KISS parser/writer for any Arduino Stream
  • NimBLE-Arduino server with BLE-KISS service + characteristics
  • Standalone KISS byte-stream parser (serial semantics over BLE)
  • Correct KISS escaping/unescaping (FEND/FESC/TFEND/TFESC)
  • Handles BLE chunk boundaries correctly:
    • partial KISS frame across writes
    • multiple KISS frames in one write
  • Fixed-size incoming stream ring buffer
  • Fixed-size decoded frame buffer
  • Fixed-size outgoing frame queue (ring)
  • MTU-aware notify chunking (ATT payload = MTU - 3)
  • Raw BLE write callback for byte chunks before KISS parsing, or normal Stream::read()
  • Callback for decoded KISS payloads in KissStreamTnc (includes command/port byte at data[0])
  • Queue helpers and light stats counters
  • Single-instance behavior is explicit (begin() fails if another instance is active)

Constraints and Design Notes

  • The library itself uses fixed-size buffers and does not allocate dynamic containers.
  • NimBLE-Arduino internals may still allocate memory internally.
  • Single active BleKissGattStream instance per template specialization.
  • KissStreamTnc::sendDataFrame() and sendKissPayload() encode frames to the attached stream.
  • BleKissGattStream::write() enqueues raw bytes; BLE notification sending happens via loop() / drainOutgoing().
  • Decoded RX frame callbacks (setFrameCallback) are dispatched from loop() context (not NimBLE callback/task context).
  • If loop() is starved, RX callback dispatch can be delayed and incoming bytes may be dropped when the fixed input ring buffer fills.
  • If Config.requireNotifySubscription is true (default), outbound data is only sent after client enables notifications.
  • On disconnect, incoming parser state and outgoing queue are cleared.

Sizing Tradeoffs

Template parameters:

BleKissGattStream<INCOMING_STREAM_SIZE, OUTGOING_CHUNK_SIZE, OUTGOING_QUEUE_DEPTH>
KissStreamTnc<DECODED_FRAME_SIZE, OUTGOING_FRAME_SIZE>
  • Larger INCOMING_STREAM_SIZE tolerates bursty BLE writes before parser drain.
  • DECODED_FRAME_SIZE is max decoded KISS payload length (command/port + payload).
  • OUTGOING_FRAME_SIZE must fit worst-case escaped encoded frame length.
    • Rough bound for payload length N: encoded size is at most N*2 + 3.
  • OUTGOING_QUEUE_DEPTH increases burst tolerance but costs static RAM.

Basic Usage

#include "BleKissGattStream.h"
#include "KissStreamTnc.h"

BleKissGattStream<512, 384, 3> bleStream;
KissStreamTnc<384, 384> kissTnc(bleStream);

static void onKissFrame(const uint8_t* data, size_t len, void* ctx) {
  (void)ctx;
  if (len == 0) return;

  uint8_t cmdPort = data[0];
  uint8_t command = cmdPort & 0x0F;
  uint8_t port = (cmdPort >> 4) & 0x0F;

  if (command == 0x00 && len > 1) {
    // data[1..] is AX.25 payload for KISS data frame
  }
}

void setup() {
  kissTnc.setFrameCallback(onKissFrame);
  kissTnc.begin();
  bleStream.begin();
}

void loop() {
  kissTnc.loop();
  bleStream.loop();
}

See:

Raw BLE GATT Stream Usage

Use BleKissGattStream when your project already owns KISS/session/protocol parsing and only needs the standard BLE-KISS GATT byte stream. If setBytesReceivedCallback() is installed, loop() dispatches and consumes incoming bytes through that callback; otherwise incoming BLE writes remain available through the normal Stream read API.

#include "BleKissGattStream.h"

using RawGatt = BleKissGattStream<512, 256, 4>;
RawGatt bleStream;

static void onBytes(const uint8_t* data, size_t len, void* ctx) {
  (void)ctx;
  // Raw app -> TNC BLE write bytes. Parse or route them in your own layer.
}

void setup() {
  bleStream.setBytesReceivedCallback(onBytes);
  bleStream.begin();
}

void loop() {
  bleStream.loop();
}

Generic KISS Stream Usage

Use KissStreamTnc with any Arduino stream, including Serial, HardwareSerial, or BleKissGattStream.

#include "KissStreamTnc.h"

KissStreamTnc<512, 1024> serialTnc(Serial);

static void onKissFrame(const uint8_t* data, size_t len, void* ctx) {
  (void)ctx;
  // data[0] is command/port, data[1..] is command payload.
}

void setup() {
  Serial.begin(115200);
  serialTnc.setFrameCallback(onKissFrame);
  serialTnc.begin();
}

void loop() {
  serialTnc.loop();
}

Examples

  1. examples/BasicBleKissTnc
  • Simple BLE-KISS usage example.
  • Includes resource tracking and empty-baseline build env for RAM/flash diffing.
  1. examples/ModemTnc
  • Real AFSK modem firmware (RF/audio path) using esp32-afsk.
  • Supports both BLE and serial KISS transports:
    • esp32dev_ble (BLE-KISS)
    • esp32dev_serial (USB serial KISS)
  • esp32-afsk is pulled via PlatformIO lib_deps (no local clone required).
  1. examples/RawBleKissGattStream
  • Raw BLE-KISS GATT byte-stream example.
  • Echoes incoming BLE write chunks through RX notifications without parsing KISS.

PlatformIO

This repo now includes library.json for PlatformIO library metadata. The example folder is also a standalone PlatformIO project:

  • examples/BasicBleKissTnc/platformio.ini (uses lib_extra_dirs = ../..)
  • examples/BasicBleKissTnc/src/main.cpp

Build the basic example project:

pio run -d examples/BasicBleKissTnc

Build modem firmware examples:

pio run -d examples/ModemTnc -e esp32dev_ble
pio run -d examples/ModemTnc -e esp32dev_serial
pio run -d examples/RawBleKissGattStream

Run host unit tests (no hardware required):

pio test -e native

Unit Tests

The repository includes host-native unit tests under test/test_kiss_core for core KISS behavior:

  • command/port byte packing
  • frame encoding and escape correctness
  • byte-stream reassembly for split frames
  • multiple frames within one stream chunk
  • malformed escape detection
  • decoded frame overflow handling

These tests validate standalone logic in src/KissCodec.h, including round-trip payloads where the decoded frame includes the command/port byte.

Resource Usage (ESP32)

Current example uses a low-RAM profile:

  • template sizes: BleKissGattStream<512, 384, 3> + KissStreamTnc<384, 384>
  • preferred MTU: 185
  • TX enqueue guarded by canSend()
  • fixed buffer estimate for this profile: 2688 bytes (estimatedStaticBufferBytes())
  • NimBLE low-RAM compile flags (see examples/BasicBleKissTnc/platformio.ini):
    • CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
    • CONFIG_BT_NIMBLE_MAX_BONDS=1
    • CONFIG_BT_NIMBLE_MAX_CCCDS=2
    • CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=8
    • CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=185
    • CONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED=1
    • CONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED=1

Library fixed-buffer estimate is available at compile time:

  • BleKissGattStream<...>::estimatedStaticBufferBytes() + KissStreamTnc<...>::estimatedStaticBufferBytes()
  • Formula: INCOMING_STREAM_SIZE + INCOMING_CALLBACK_CHUNK_SIZE + OUTGOING_QUEUE_DEPTH * (OUTGOING_FRAME_SIZE + sizeof(size_t)) + DECODED_FRAME_SIZE + OUTGOING_FRAME_SIZE

Baseline diff workflow (empty app baseline):

# compares:
# - esp32dev_baseline (empty app, no BLE-KISS instances)
# - esp32dev (full example)
scripts/resource_diff.sh

Latest measured resource_diff.sh output:

  • RAM baseline: 21464 / 327680
  • RAM full: 37676 / 327680
  • RAM delta: +16212 bytes
  • Flash baseline: 267137 / 1310720
  • Flash full: 589481 / 1310720
  • Flash delta: +322344 bytes

Runtime heap probe in example:

  • The example prints [heap] snapshots at:
    • boot
    • afterBegin
    • onConnect
    • onDisconnect
    • periodic (every 10s)
  • Fields:
    • free: current free heap (ESP.getFreeHeap())
    • min: minimum-ever free heap (ESP.getMinFreeHeap())
    • largest: largest free 8-bit block (heap_caps_get_largest_free_block(MALLOC_CAP_8BIT))
    • deltaBoot: free - free_at_boot

API Summary

Raw GATT stream:

  • BleKissGattStream<INCOMING_STREAM_SIZE, OUTGOING_CHUNK_SIZE, OUTGOING_QUEUE_DEPTH>
  • Arduino Stream: available(), read(), peek(), flush(), write(...)
  • setBytesReceivedCallback(BytesCallback cb, void* ctx = nullptr)
  • setConnectCallback(EventCallback cb, void* ctx = nullptr)
  • setDisconnectCallback(EventCallback cb, void* ctx = nullptr)
  • bool sendBytes(const uint8_t* data, size_t len)
  • bool isConnected() const
  • bool isNotifySubscribed() const
  • bool canSend() const
  • uint16_t getMtu() const
  • queue capacity/backpressure helpers
  • const Stats& stats() const

Generic KISS stream TNC:

  • KissStreamTnc<DECODED_FRAME_SIZE, OUTGOING_FRAME_SIZE>
  • void loop()
  • setFrameCallback(FrameCallback cb, void* ctx = nullptr)
  • bool sendDataFrame(const uint8_t* payload, size_t len, uint8_t port = 0)
  • bool sendKissPayload(const uint8_t* kissPayload, size_t len)
  • const Stats& stats() const
  • void clearStats()

Notes for BLE-KISS Clients

This is intended for BLE-KISS capable clients/apps implementing the BLE-KISS spec, rather than as a Classic Bluetooth SPP transport for APRSdroid.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages