Skip to content

Transfer Protocol

The transfer protocol fragments JPEG images into LoRa-sized packets with Base64 encoding, CRC16 integrity verification, and windowed ACK reliability.

All packets are ASCII text, max 240 characters (the RYLR AT+SEND limit).

Sent once at the start of each transfer.

H:<id>:<total_frags>:<total_bytes>:<width>:<height>:<crc16>
FieldTypeDescription
iduint16Transfer ID (monotonically increasing)
total_fragsuint16Total number of data fragments
total_bytessize_tJPEG file size in bytes
widthuint16Image width in pixels
heightuint16Image height in pixels
crc16hexCRC16-CCITT over the full JPEG data

Example: H:1:51:8432:320:240:A3F2

One per fragment. Carries Base64-encoded JPEG data.

D:<id>:<frag_num>:<base64_data>
FieldTypeDescription
iduint16Transfer ID (must match header)
frag_numuint16Fragment index (0-based)
base64_datastringBase64-encoded raw JPEG bytes

Example: D:1:0:/9j/4AAQSkZJR...

Sent by the receiver after each window of data fragments.

A:<id>:<window_start>:<bitmap_hex>
FieldTypeDescription
iduint16Transfer ID
window_startuint16First fragment index in the window
bitmap_hexhex8-bit bitmap: bit N set = fragment (window_start + N) received

Example: A:1:0:FF (all 8 fragments in window received)

Example: A:1:0:F7 (fragment 3 missing — binary 11110111)

Sent by either side to cancel a transfer.

N:<id>:<reason>
ReasonDescription
NO_MEMReceiver cannot allocate reassembly buffer
SEND_FAILSender failed to transmit

Remote control from base station to camera.

C:CAPTURE
C:STATUS
C:PING
C:CONFIG:key=val
CommandDescription
CAPTURETrigger immediate image capture and transfer
STATUSRequest status report
PINGKeepalive check (responds with S:PONG)
CONFIG:key=valSet configuration: period, motion, threshold

Camera status response.

S:HEAP=N:UP=N:RSSI=N:CAP=N:VER=X
FieldDescription
HEAPFree heap memory in bytes
UPUptime in seconds
RSSILast received signal strength (dBm)
CAPTotal captures since boot
VERFirmware version string
ConstantValueDerivation
LORA_MAX_PAYLOAD240AT+SEND hard limit
LORA_PKT_OVERHEAD16D:<id>:<frag>: prefix (conservative)
LORA_B64_PER_FRAG224(240 - 16) / 4 * 4 (multiple of 4 for Base64)
LORA_RAW_PER_FRAG168224 * 3 / 4 (Base64 decode ratio)
LORA_WINDOW_SIZE8Fits in a single-byte ACK bitmap
LORA_MAX_RETRIES3Per window
LORA_ACK_TIMEOUT_MS5000Wait for ACK per window
LORA_INTER_PKT_MS50Delay between data packets
sequenceDiagram
    participant S as Sender
    participant R as Receiver

    S->>R: H:1:51:8432:320:240:A3F2
    Note right of R: Allocate PSRAM buffer

    S->>R: D:1:0:⟨b64⟩
    S->>R: D:1:1:⟨b64⟩
    Note over S,R: ... fragments 2–6 ...
    S->>R: D:1:7:⟨b64⟩

    R->>S: A:1:0:FF
    Note left of S: All 8 received

    S->>R: D:1:8:⟨b64⟩
    Note over S,R: ... fragments 9–14 ...
    S->>R: D:1:15:⟨b64⟩

    R->>S: A:1:8:FB
    Note left of S: Fragment 10 missing

    S->>R: D:1:10:⟨b64⟩
    Note right of R: Retransmit

    R->>S: A:1:8:FF
    Note left of S: Window complete

    Note over S,R: Continue until all fragments ACK'd

CRC16 CCITT-FALSE with polynomial 0x1021 and initial value 0xFFFF:

uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}

The same CRC implementation exists in both the C firmware and Python base station to ensure verification compatibility.

Binary packet format (progressive pipeline)

Section titled “Binary packet format (progressive pipeline)”

The progressive pipeline (cam_pjpeg.h) uses a binary wire format instead of ASCII. All packets are sent via lora_send_binary() / lora_receive_binary() — no Base64 encoding.

packet-beta
title Data Packet Header (3 bytes)
0-7: "image_id"
8-11: "segment"
12-15: "flags"
16-23: "frag_index"

Header is followed by variable-length payload, then an optional CRC8 byte.

ByteFieldDescription
0image_idWrapping image counter (0–255)
1segment:4 | flags:4Upper nibble: segment index (0–14 = scan, 15 = JPEG headers). Lower nibble: flags
2fragment_indexFragment within segment (0–255)
3..payloadRaw JPEG data (headers, scan entropy, or EOI marker)
lastCRC8Optional CRC8-CCITT over header + payload (when use_crc8 is true)

Flags (lower 4 bits of byte 1):

FlagValueDescription
FIRST0x01First fragment of segment
LAST0x02Last fragment of segment
HDR0x04JPEG header segment (SOI, DQT, SOF2, DHT)
EOI0x08Image complete after this packet

Sent by receiver to acknowledge a window of data packets.

packet-beta
title ACK Packet (4 bytes)
0-7: "image_id"
8-15: "0xFF (ACK)"
16-23: "segment"
24-31: "bitmap"
ByteFieldDescription
0image_idMust match current transfer
10xFFACK marker (cannot collide with valid data byte 1)
2segmentSegment index being acknowledged
3bitmapBit N set = fragment (window_start + N) received

Sent by receiver to reject a segment.

packet-beta
title NACK Packet (4 bytes)
0-7: "image_id"
8-15: "0xFE (NACK)"
16-23: "segment"
24-31: "reason"
ByteFieldDescription
0image_idTransfer being rejected
10xFENACK marker
2segmentSegment index
3reasonReason code (see table)

NACK reasons:

CodeNameDescription
0x01NO_MEMReceiver cannot allocate reassembly buffer
0x02CRC_FAILCRC8 verification failed
0x03ABORTReceiver aborted transfer
ConstantValueDescription
Header size3 bytesimage_id + segment/flags + fragment_index
ACK/NACK size4 bytesFixed size, sent via lora_send_binary()
ACK marker0xFFDistinguishes ACK from data (byte 1)
NACK marker0xFEDistinguishes NACK from data (byte 1)
Window size8Default fragments per ACK window
Max retries3Per window
ACK timeout5000 msWait for ACK per window
Inter-packet delay50 msBetween data packets

CRC8-CCITT with polynomial x^8 + x^2 + x + 1 (0x07), initial value 0x00:

uint8_t pjpeg_lora_crc8(const uint8_t *data, size_t len);

Uses a 256-byte lookup table stored in flash. When enabled, CRC8 is appended as the last byte of each data packet. The receiver verifies before updating its reassembly state.