Architecture
The system has two sides: the camera node (ESP32-CAM firmware) and the base station (Python CLI on a PC). They communicate over LoRa using RYLR modules.
Component diagram
Section titled “Component diagram”flowchart TD
subgraph ESP["ESP32-CAM Firmware"]
subgraph Legacy["Legacy Pipeline — default"]
CL["cam_lora.h<br/>FreeRTOS task: periodic · on-demand · motion"]
LT["lora_transfer.h<br/>Fragment + Base64 + CRC16 + Window ACK"]
MD["motion_detect.h<br/>JPEG size heuristic"]
end
subgraph Progressive["Progressive Pipeline — USE_PROGRESSIVE_JPEG"]
CP["cam_pjpeg.h<br/>FreeRTOS task: periodic · on-demand"]
EP["esp_pjpeg<br/>RGB565 → PJPEG"]
PLA["pjpeg_lora_ack<br/>CRC8 + Window ACK/retransmit"]
end
LORA["lora_rylr.h<br/>AT+SEND / +RCV<br/>lora_send · lora_send_binary · lora_receive_binary<br/>UART on GPIO 1/3"]
CAM["OV2640 camera"]
end
CL --> LT --> LORA
CL --> MD
CP --> EP --> PLA --> LORA
LORA ---|"LoRa 915 MHz"| RYLR
subgraph BS["Base Station — Python"]
RYLR["RYLRModule<br/>serial AT driver"]
IR["ImageReceiver<br/>Fragment reassembly + CRC verify + ACK bitmap"]
CLI["BaseStation CLI<br/>capture · status · ping · config · list"]
end
RYLR --> IR --> CLI
main.c includes either cam_lora.h or cam_pjpeg.h based on USE_PROGRESSIVE_JPEG. Both pipelines share lora_rylr.h for radio access but use different encoding and fragmentation layers.
Data flow: legacy pipeline (cam_lora.h)
Section titled “Data flow: legacy pipeline (cam_lora.h)”-
Trigger — the
cam_loratask decides to capture based on periodic timer, motion detection, or a remoteC:CAPTUREcommand. -
Capture —
fake_camera_fb_get()(QEMU) oresp_camera_fb_get()(hardware) returns a JPEG framebuffer. The JPEG is copied to PSRAM and the camera framebuffer is released. -
Fragment —
xfer_send_image()computes CRC16 over the JPEG, calculates fragment count (ceil(jpeg_len / 168)), and sends a header packet. -
Encode + Send — each fragment’s raw bytes are Base64-encoded and wrapped in a
D:packet. Fragments are sent in windows of 8. -
ACK — the base station receives fragments and replies with an
A:bitmap per window. Missing fragments are retransmitted up to 3 times. -
Reassemble —
ImageReceivercollects all fragments, reassembles the JPEG, verifies CRC16, and saves to disk with a timestamp filename.
Data flow: progressive pipeline (cam_pjpeg.h)
Section titled “Data flow: progressive pipeline (cam_pjpeg.h)”-
Trigger — the
cam_pjpegtask captures on periodic timer or remoteC:CAPTUREcommand. -
Capture — gets an RGB565 framebuffer (320x240, 153.6 KB). Pixels are copied to PSRAM and the camera framebuffer is released.
-
Encode —
pjpeg_encode_coefficients()performs color conversion, DCT, and quantization. Coefficients are stored in PSRAM, organized into progressive scans (DC first, then AC refinement). -
Packetize —
pjpeg_lora_ack_send_image()iterates over JPEG headers and each scan. For each segment,pjpeg_lorafragments the data into MTU-sized binary packets with 3-byte headers. If CRC8 is enabled, one integrity byte is appended. -
Transmit — each packet is sent via
lora_send_binary(). If ACK mode is enabled, packets are sent in windows of 8, and the sender waits for a 4-byte ACK bitmap vialora_receive_binary(). Missing packets are retransmitted. -
Reassemble — the receiver reconstructs the progressive JPEG scan-by-scan. Early scans produce a low-detail preview; later scans refine it.
Conditional compilation
Section titled “Conditional compilation”The firmware uses #ifdef guards to switch between QEMU and hardware modes:
| Define | Effect |
|---|---|
CONFIG_QEMU_TEST_MODE | Disables WiFi/BLE, uses fake camera, enables LoRa loopback queue |
CONFIG_ESP_WIFI_ENABLED | Enables WiFi manager and remote logging |
CONFIG_BT_ENABLED | Enables BLE provisioning |
CONFIG_SPIRAM | Enables PSRAM-backed allocations |
USE_PROGRESSIVE_JPEG | Switches main.c from cam_lora.h to cam_pjpeg.h pipeline |
Header-only design
Section titled “Header-only design”All firmware components are implemented as static inline functions in .h files. This is intentional for the ESP-IDF single-compilation-unit model:
- No link-time symbol conflicts
- Functions are inlined for performance
- Single
main.cpulls in all headers
The exception is fake_camera.c, which is a separate ESP-IDF component with its own CMakeLists.txt.