Skip to content

Design Decisions

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:

GPIOTrade-off
1 (TX)Sacrifices serial console
3 (RX)Sacrifices serial console
13Loses SD card DATA3
14Loses 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.

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.

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:

SettingValueRationale
Window size8Fits in a single-byte bitmap
ACK timeout5 secondsAccounts for LoRa airtime at SF9-12
Inter-packet delay50msPrevents receiver buffer overflow
Max retries3Balances reliability vs. total transfer time

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.

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

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 UART
  • lora_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.