The KISS File Transfer Protocol is a robust suite of applications designed for reliable file transfer over RF with any KISS compatible TNC and uses standard AX.25 UI frames (i.e. does not require 'connected mode'). The system consists of a sender, receiver and optional fileserver applications, as well as dedicated applications for repeating and bridging. A pure HTML/JS implementation is provided with support for Chrome's Serial API as well as an optional websockets application. The protocol divides a file into a header packet containing metadata and multiple data packets carrying file chunks. It also supports control packets including ACKs (acknowledgments) and command (CMD) and response (RSP) messages.
The protocol supports both raw binary and Base64-encoded payloads, optional zlib compression, and a dynamic sliding window with adaptive timeouts and retransmissions.
The protocol defines several key packet types:
Additionally, each packet includes explicit fields such as a unique FILEID, encoding method (0 for binary, 1 for Base64), and a compression flag.
All packets are encapsulated in a KISS frame to preserve data boundaries. The process is as follows:
0xC0 marks the beginning of a frame.0x00 for data packets.0xC0 and 0xDB) within the payload are replaced with escape sequences to prevent conflicts with framing markers.0xC0 byte signals the end of the frame.
function buildKISSFrame(packet):
FLAG = 0xC0
CMD = 0x00
escaped_packet = escapeSpecialBytes(packet)
return FLAG + CMD + escaped_packet + FLAG
Each packet begins with a fixed 16-byte header modeled after AX.25 addressing. This header includes:
0x03 and 0xF0.
function buildAX25Header(source, destination):
dest = encodeAddress(destination, is_last = False)
src = encodeAddress(source, is_last = True)
CONTROL = 0x03
PID = 0xF0
return dest + src + CONTROL + PID
Every packet consists of:
FILEID, encodingMethod, and compress_flag.DATA packets are used to transmit file data. They come in two types:
The header packet initializes a file transfer and carries vital metadata. Its info field is formatted as:
"FILEID:0001{burst_to_hex}/{total_hex}:"
Where:
FILEID is the unique file identifier.0001 indicates that this is the header packet.{burst_to_hex} is the burst boundary expressed in 4-digit hexadecimal.{total_hex} is the total number of data packets (also in 4-digit hexadecimal).Data packets carry sequential chunks of file data. Their info field is formatted as:
"FILEID:{seq_hex}{burst_to_hex}:"
Where:
{seq_hex} is the 4-digit hexadecimal sequence number (starting from 0002).{burst_to_hex} indicates the burst boundary for the current window.CHUNK_SIZE in bytes). If Base64 encoding is enabled, each chunk is individually encoded.
ACK packets acknowledge received data. Their info field contains fields separated by colons. A cumulative ACK, for example "FILEID:ACK:0001-XXXX", indicates the highest contiguous packet received.
CMD Packets: Issued by remote clients, CMD packets enable file operations (such as GET, LIST, or PUT). Their info field is constructed as follows:
"cmdID:CMD:"
In this format:
cmdID is a two-character command identifier generated for the command.CMD is the literal string indicating a command.<command text> is the operation to be performed (for example, “GET myfile.txt”, “LIST”, or “PUT myfile.txt”).RSP Packets: Sent in response to CMD packets, RSP packets provide the result of the requested operation. Their info field is formatted as:
"cmdID:RSP:: "
Here:
cmdID is the two-character identifier corresponding to the original CMD packet.RSP is the literal string indicating a response.<status> is an integer value where 1 denotes success and 0 denotes failure.<message> is a brief description of the outcome.
The receiver processes incoming KISS frames via a FrameReader, parses packets, and reassembles file transfers. Key steps include:
// Initialize connection and frame reader
conn = openConnection() // TCP or serial
frameChan = newChannel()
reader = new FrameReader(conn, frameChan)
start(reader.Run())
transfers = {} // Map FILEID -> Transfer state
while true:
if frame received from frameChan:
packet = parsePacket(frame)
if packet is ACK:
continue
if no transfer exists for packet.FILEID:
if packet.seq != 1:
continue
metadata = split(packet.payload, "|")
transfers[packet.FILEID] = new Transfer(
filename = metadata[2],
origSize = metadata[3],
compSize = metadata[4],
md5 = metadata[5],
encodingMethod = metadata[7],
compress = (metadata[8] == "1"),
totalPackets = metadata[9]
)
sendAck(conn, localCallsign, packet.sender, packet.FILEID, "0001")
else:
transfer = transfers[packet.FILEID]
if packet.seq == 1:
continue
if packet.seq not in transfer.Packets:
transfer.Packets[packet.seq] = packet.payload
if packet.seq equals transfer.BurstTo:
ackValue = computeCumulativeAck(transfer)
sendAck(conn, localCallsign, packet.sender, packet.FILEID, ackValue)
else if timeout:
for each transfer in transfers:
if inactivity exceeds retry interval:
if retries not exceeded:
resend cumulative ACK for transfer
else:
drop transfer
for each transfer in transfers:
if all expected packets received:
fileData = concatenate packets in order
if encodingMethod == 1:
fileData = base64Decode(fileData)
if compress:
fileData = decompress(fileData)
if md5(fileData) matches transfer.md5:
processFile(fileData, transfer.filename)
send final ACK (FIN-ACK)
remove transfer from transfers
To ensure robustness, the receiver monitors TCP inactivity. If no data is received within a configured deadline, the receiver reconnects.
tcp_timeout = configured deadline (e.g., 600 seconds)
while true:
sleep(1 second)
if currentTime - lastDataTime > tcp_timeout:
log "Inactivity detected, reconnecting..."
reader.Stop()
conn.Close()
loop until new connection is established:
wait 5 seconds
attempt new connection
update lastDataTime
restart FrameReader with new connection
The sender implements a dynamic sliding window, while the receiver computes a cumulative ACK based on the highest contiguous packet received and retransmits ACKs if necessary.
function computeCumulativeAck(transfer):
max_seq = 1
for seq = 2 to transfer.totalPackets:
if transfer.Packets contains seq:
max_seq = seq
else:
break
if max_seq == 1:
return "0001"
else:
return "0001-" + toHex(max_seq, 4)
The ACK is then sent encapsulated within a KISS frame.
The receiver application can be invoked as follows:
# Receiver that saves files to disk
./receiver --my-callsign MM3NDH-11 --host 0.0.0.0 --port 9001 --one-file --callsigns "MM5NDH-*,*-15"
# Receiver that outputs the received file to stdout (for piping)
./receiver --my-callsign MM3NDH-11 --host 0.0.0.0 --port 9001 --one-file --stdout
# Receiver that executes a file if its name matches the --execute parameter
./receiver --my-callsign MM3NDH-11 --host 0.0.0.0 --port 9001 --one-file --execute "update.sh"
The diagram below shows a typical deployment scenario:
+------------------------------------------+
| Sender |
| (Serial TNC on 144 MHz, e.g., COM3) |
+----------------------+-------------------+
| RF Link (144 MHz)
|
v
+------------------------------+
| Proxy / Fileserver (CMD/RSP)|
| - Processes CMD packets |
| - Forwards DATA and ACK |
| - Monitors TCP activity |
+--------------+---------------+
| RF Link (433 MHz)
|
v
+------------------------------------------+
| Receiver |
| (TCP/Serial TNC on 433 MHz) |
| - Reassembles file from DATA packets |
| - Sends cumulative ACKs & FIN-ACK |
+------------------------------------------+
The KISS File Transfer Protocol combines robust framing (using KISS and AX.25), explicit metadata, and dynamic control mechanisms to enable reliable file transfers in challenging RF environments.
With support for DATA, ACK, CMD, and RSP packets, optional zlib compression, and Base64 encoding on a per-chunk basis, the sender and receiver collaborate using dynamic sliding windows, adaptive timeouts, and reconnection logic to maximize throughput while ensuring data integrity.
CMD packets follow the format "<cmdID>:CMD:<command text>", and the corresponding RSP packets use the format "<cmdID>:RSP:<status>:<message>", where the cmdID links the request and its response.