Design Decisions
GPIO 1/3 for LoRa UART
Section titled “GPIO 1/3 for LoRa UART”The ESP32-CAM has almost no free GPIO pins. Camera uses 15 pins, SD card uses 6, and PSRAM claims GPIO16. That leaves only four candidates:
| GPIO | Trade-off |
|---|---|
| 1 (TX) | Sacrifices serial console |
| 3 (RX) | Sacrifices serial console |
| 13 | Loses SD card DATA3 |
| 14 | Loses SD card CLK |
Decision: Use GPIO 1/3 (the console UART) for production. This keeps PSRAM and SD card fully available. The trade-off is no serial console, which means OTA updates become mandatory for field firmware updates.
For development, GPIO 13/14 can be used instead — see GPIO Pinout reference for both configurations.
Base64 encoding for LoRa payloads
Section titled “Base64 encoding for LoRa payloads”The RYLR modules use AT+SEND=addr,len,data where data must be printable ASCII (the module interprets raw bytes differently). Base64 encoding ensures safe transport at a 33% overhead cost.
Fragment math:
- Max AT+SEND payload: 240 characters
- Packet overhead (
D:1:42:prefix): ~16 characters - Base64 payload per fragment: 224 characters (multiple of 4)
- Raw bytes per fragment: 168 bytes
A 10KB JPEG requires ~60 fragments.
Windowed ACK protocol
Section titled “Windowed ACK protocol”Sending one fragment at a time and waiting for an ACK would be extremely slow — each LoRa packet takes hundreds of milliseconds at SF9. Instead, fragments are sent in windows of 8 before waiting for a single ACK bitmap.
The ACK is a single byte where each bit represents a fragment in the window. Missing fragments are retransmitted up to 3 times before the transfer is aborted.
Parameters:
| Setting | Value | Rationale |
|---|---|---|
| Window size | 8 | Fits in a single-byte bitmap |
| ACK timeout | 5 seconds | Accounts for LoRa airtime at SF9-12 |
| Inter-packet delay | 50ms | Prevents receiver buffer overflow |
| Max retries | 3 | Balances reliability vs. total transfer time |
JPEG size heuristic for motion detection
Section titled “JPEG size heuristic for motion detection”Traditional motion detection compares pixel-by-pixel differences between frames. On ESP32-CAM this is expensive — a QVGA frame is 150KB uncompressed.
Insight: When the OV2640 JPEG-compresses a changed scene, the file size changes significantly. A static scene produces consistent JPEG sizes (±2-3%). A moving subject causes >15% variation.
This gives us motion detection for free — just compare fb->len between consecutive captures. No extra memory, no extra processing, and it works with the fake camera in QEMU.
Debounce: requires two consecutive above-threshold changes to trigger, avoiding false positives from compression noise.
OTA as a requirement, not an option
Section titled “OTA as a requirement, not an option”Choosing GPIO 1/3 for LoRa means the UART programming port is repurposed. You physically cannot flash the ESP32 without disconnecting the LoRa module. OTA with dual app partitions (ota_0 / ota_1) makes field updates viable:
- HTTP OTA — connects to WiFi and downloads firmware from a web server
- GPIO0 trigger — holding the K1 button for 3 seconds during boot enters OTA mode
- Dual partitions — if an update fails, the device boots the previous working firmware
QEMU loopback testing
Section titled “QEMU loopback testing”Real LoRa hardware isn’t available in CI or during rapid iteration. The firmware uses CONFIG_QEMU_TEST_MODE to replace UART I/O with a FreeRTOS queue:
lora_send()writes packets to a queue instead of UARTlora_receive()reads from the same queue- The full fragmentation/Base64/CRC pipeline runs end-to-end
This verifies protocol correctness without touching any hardware. The test_loopback_transfer() function in main.c sends the embedded 64x64 test image through the pipeline and verifies the reassembled output matches byte-for-byte.