Skip to content

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.

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.

  1. Trigger — the cam_lora task decides to capture based on periodic timer, motion detection, or a remote C:CAPTURE command.

  2. Capturefake_camera_fb_get() (QEMU) or esp_camera_fb_get() (hardware) returns a JPEG framebuffer. The JPEG is copied to PSRAM and the camera framebuffer is released.

  3. Fragmentxfer_send_image() computes CRC16 over the JPEG, calculates fragment count (ceil(jpeg_len / 168)), and sends a header packet.

  4. Encode + Send — each fragment’s raw bytes are Base64-encoded and wrapped in a D: packet. Fragments are sent in windows of 8.

  5. ACK — the base station receives fragments and replies with an A: bitmap per window. Missing fragments are retransmitted up to 3 times.

  6. ReassembleImageReceiver collects 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)”
  1. Trigger — the cam_pjpeg task captures on periodic timer or remote C:CAPTURE command.

  2. Capture — gets an RGB565 framebuffer (320x240, 153.6 KB). Pixels are copied to PSRAM and the camera framebuffer is released.

  3. Encodepjpeg_encode_coefficients() performs color conversion, DCT, and quantization. Coefficients are stored in PSRAM, organized into progressive scans (DC first, then AC refinement).

  4. Packetizepjpeg_lora_ack_send_image() iterates over JPEG headers and each scan. For each segment, pjpeg_lora fragments the data into MTU-sized binary packets with 3-byte headers. If CRC8 is enabled, one integrity byte is appended.

  5. 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 via lora_receive_binary(). Missing packets are retransmitted.

  6. Reassemble — the receiver reconstructs the progressive JPEG scan-by-scan. Early scans produce a low-detail preview; later scans refine it.

The firmware uses #ifdef guards to switch between QEMU and hardware modes:

DefineEffect
CONFIG_QEMU_TEST_MODEDisables WiFi/BLE, uses fake camera, enables LoRa loopback queue
CONFIG_ESP_WIFI_ENABLEDEnables WiFi manager and remote logging
CONFIG_BT_ENABLEDEnables BLE provisioning
CONFIG_SPIRAMEnables PSRAM-backed allocations
USE_PROGRESSIVE_JPEGSwitches main.c from cam_lora.h to cam_pjpeg.h pipeline

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.c pulls in all headers

The exception is fake_camera.c, which is a separate ESP-IDF component with its own CMakeLists.txt.