Giving an I²C packet modem a serial interface
How i2ckiss-ng translates between a TNC-Pi or TNC-Black and an ordinary KISS client, including the bytes that need changing.
On this page
APRX can talk to a packet modem using KISS over a serial device, but a TNC-Pi or TNC-Black can present its modem interface over I²C.
i2ckiss-ng sits between them. It gives the application a pseudo-terminal, opens the Linux I²C device on the other side, and translates the framing as bytes pass through. The first release was published on 1 September; the examples here follow version 2.0.1. It is an independent implementation of the interface associated with John Wiseman G8BPQ’s original i2ckiss work.
The application can open a path such as /run/i2ckiss-tnc0/pty while the bridge deals with /dev/i2c-1 and the modem’s bus address. The Linux AX.25 stack is another possible consumer, but it isn’t required just to give a KISS application that serial-looking endpoint.
The payload can contain the framing bytes
KISS uses C0 as a frame delimiter. A binary payload may also contain a byte whose value is C0, so that byte has to be escaped when it appears inside a frame. The escape byte, DB, needs escaping too:
| Byte inside the frame | Bytes sent on the wire |
|---|---|
C0 | DB DC |
DB | DB DD |
Take the short command-and-data sequence 00 01 C0 DB 7F. The first byte is the KISS command and port byte; the remaining values are chosen to include both special cases. This is an illustration of framing, not a complete AX.25 radio packet.
The serial representation is:
C0 00 01 DB DC DB DD 7F C0
The I²C transport adds one more piece: an XOR byte calculated across the unescaped command and data. For this example:
00 XOR 01 XOR C0 XOR DB XOR 7F = 65
That gives the I²C side:
C0 00 01 DB DC DB DD 7F 65 C0
The released encoder produces those sequences; they were checked on 2 October 2026 by compiling and running that encoder with this input. The extra byte belongs to the local I²C transport. It isn’t the AX.25 frame check sequence used on the radio link.
On the return path, the bridge checks and removes the I²C XOR byte before making a serial KISS frame. It also uses KISS port zero for its endpoint. A direct byte-for-byte copy would leave the serial client seeing data that belongs to the I²C transport.
Reads introduce another boundary of their own. An operating-system read can end just after DB, leaving the escaped value to arrive in the next read. The decoder has to retain that state. The public tests include this split-read case, along with malformed and oversized frames.
Keeping the endpoint useful when the bus stops responding
Framing is only part of running the bridge. If the I²C connection fails, the reconnect path closes that descriptor, resets the decoder and schedules another attempt with exponential backoff. The default delay grows from 250 milliseconds up to thirty seconds. A partially sent queued frame starts again at its beginning when the connection returns.
That retry doesn’t establish exactly-once delivery of an application packet. The queues are bounded as well, so a modem that remains unavailable cannot consume memory indefinitely while a client continues writing.
The pseudo-terminal is held independently of the I²C connection, and a stable symlink gives the application a predictable name to open. A daemon restart can still require a client to reconnect: an already-open descriptor doesn’t move to a newly created PTY just because the symlink now points there.