Transfer Protocol
The transfer protocol fragments JPEG images into LoRa-sized packets with Base64 encoding, CRC16 integrity verification, and windowed ACK reliability.
Packet types
Section titled “Packet types”All packets are ASCII text, max 240 characters (the RYLR AT+SEND limit).
Header (H)
Section titled “Header (H)”Sent once at the start of each transfer.
H:<id>:<total_frags>:<total_bytes>:<width>:<height>:<crc16>| Field | Type | Description |
|---|---|---|
id | uint16 | Transfer ID (monotonically increasing) |
total_frags | uint16 | Total number of data fragments |
total_bytes | size_t | JPEG file size in bytes |
width | uint16 | Image width in pixels |
height | uint16 | Image height in pixels |
crc16 | hex | CRC16-CCITT over the full JPEG data |
Example: H:1:51:8432:320:240:A3F2
Data (D)
Section titled “Data (D)”One per fragment. Carries Base64-encoded JPEG data.
D:<id>:<frag_num>:<base64_data>| Field | Type | Description |
|---|---|---|
id | uint16 | Transfer ID (must match header) |
frag_num | uint16 | Fragment index (0-based) |
base64_data | string | Base64-encoded raw JPEG bytes |
Example: D:1:0:/9j/4AAQSkZJR...
ACK bitmap (A)
Section titled “ACK bitmap (A)”Sent by the receiver after each window of data fragments.
A:<id>:<window_start>:<bitmap_hex>| Field | Type | Description |
|---|---|---|
id | uint16 | Transfer ID |
window_start | uint16 | First fragment index in the window |
bitmap_hex | hex | 8-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)
Abort (N)
Section titled “Abort (N)”Sent by either side to cancel a transfer.
N:<id>:<reason>| Reason | Description |
|---|---|
NO_MEM | Receiver cannot allocate reassembly buffer |
SEND_FAIL | Sender failed to transmit |
Command (C)
Section titled “Command (C)”Remote control from base station to camera.
C:CAPTUREC:STATUSC:PINGC:CONFIG:key=val| Command | Description |
|---|---|
CAPTURE | Trigger immediate image capture and transfer |
STATUS | Request status report |
PING | Keepalive check (responds with S:PONG) |
CONFIG:key=val | Set configuration: period, motion, threshold |
Status (S)
Section titled “Status (S)”Camera status response.
S:HEAP=N:UP=N:RSSI=N:CAP=N:VER=X| Field | Description |
|---|---|
HEAP | Free heap memory in bytes |
UP | Uptime in seconds |
RSSI | Last received signal strength (dBm) |
CAP | Total captures since boot |
VER | Firmware version string |
Protocol constants
Section titled “Protocol constants”| Constant | Value | Derivation |
|---|---|---|
LORA_MAX_PAYLOAD | 240 | AT+SEND hard limit |
LORA_PKT_OVERHEAD | 16 | D:<id>:<frag>: prefix (conservative) |
LORA_B64_PER_FRAG | 224 | (240 - 16) / 4 * 4 (multiple of 4 for Base64) |
LORA_RAW_PER_FRAG | 168 | 224 * 3 / 4 (Base64 decode ratio) |
LORA_WINDOW_SIZE | 8 | Fits in a single-byte ACK bitmap |
LORA_MAX_RETRIES | 3 | Per window |
LORA_ACK_TIMEOUT_MS | 5000 | Wait for ACK per window |
LORA_INTER_PKT_MS | 50 | Delay between data packets |
Window protocol flow
Section titled “Window protocol flow”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 implementation
Section titled “CRC16 implementation”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.
Data packet
Section titled “Data packet”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.
| Byte | Field | Description |
|---|---|---|
| 0 | image_id | Wrapping image counter (0–255) |
| 1 | segment:4 | flags:4 | Upper nibble: segment index (0–14 = scan, 15 = JPEG headers). Lower nibble: flags |
| 2 | fragment_index | Fragment within segment (0–255) |
| 3.. | payload | Raw JPEG data (headers, scan entropy, or EOI marker) |
| last | CRC8 | Optional CRC8-CCITT over header + payload (when use_crc8 is true) |
Flags (lower 4 bits of byte 1):
| Flag | Value | Description |
|---|---|---|
FIRST | 0x01 | First fragment of segment |
LAST | 0x02 | Last fragment of segment |
HDR | 0x04 | JPEG header segment (SOI, DQT, SOF2, DHT) |
EOI | 0x08 | Image complete after this packet |
ACK packet (4 bytes)
Section titled “ACK packet (4 bytes)”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"
| Byte | Field | Description |
|---|---|---|
| 0 | image_id | Must match current transfer |
| 1 | 0xFF | ACK marker (cannot collide with valid data byte 1) |
| 2 | segment | Segment index being acknowledged |
| 3 | bitmap | Bit N set = fragment (window_start + N) received |
NACK packet (4 bytes)
Section titled “NACK packet (4 bytes)”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"
| Byte | Field | Description |
|---|---|---|
| 0 | image_id | Transfer being rejected |
| 1 | 0xFE | NACK marker |
| 2 | segment | Segment index |
| 3 | reason | Reason code (see table) |
NACK reasons:
| Code | Name | Description |
|---|---|---|
0x01 | NO_MEM | Receiver cannot allocate reassembly buffer |
0x02 | CRC_FAIL | CRC8 verification failed |
0x03 | ABORT | Receiver aborted transfer |
Binary protocol constants
Section titled “Binary protocol constants”| Constant | Value | Description |
|---|---|---|
| Header size | 3 bytes | image_id + segment/flags + fragment_index |
| ACK/NACK size | 4 bytes | Fixed size, sent via lora_send_binary() |
| ACK marker | 0xFF | Distinguishes ACK from data (byte 1) |
| NACK marker | 0xFE | Distinguishes NACK from data (byte 1) |
| Window size | 8 | Default fragments per ACK window |
| Max retries | 3 | Per window |
| ACK timeout | 5000 ms | Wait for ACK per window |
| Inter-packet delay | 50 ms | Between data packets |
CRC8 implementation
Section titled “CRC8 implementation”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.