A camera the size of a postage stamp that never joins WiFi and has no route to the internet. It sends stills over the air to a second board plugged into a Raspberry Pi, and the Pi does the rest like any other SparrowMap node. For the spot with no wire and no signal.
| Part | Which one | Why |
|---|---|---|
| Camera | AI-Thinker ESP32-CAM with the OV2640 lens. Lucian used the Hosyond kit. | The board's PSRAM is what allows VGA stills. |
| USB-TTL dongle | A CP2102 adapter, e.g. this one. Some kits include one. | The camera board has no USB port of its own. |
| Receiver | Any ESP32. Lucian used an ESP32-S3 DevKit with an external IPEX antenna. | That antenna is the range. Any board with a good link to the camera works. |
| Host | A Raspberry Pi, or any computer with a USB port. | Runs the bridge script and the SparrowMap node. |
Order matters: the receiver first, because the camera needs its address; then the keys; then the camera; then the receiver once more with the camera's address. Every board prints what the next step needs on its serial console.
Install the esp32 by Espressif board package, version 3.x. No other libraries are needed. Download the two sketch folders and the Pi script from firmware/esp32cam-espnow.
Copy receiver/config.example.h to receiver/config.h
next to it. Board: ESP32S3 Dev Module for an S3 (USB CDC On Boot:
Enabled), or ESP32 Dev Module for a plain ESP32. Upload. Open the
Serial Monitor at 921600, Both NL & CR. If you see garbage, unplug
and replug. It prints:
Receiver MAC: AC:27:6E:A4:D3:38 WARNING: no transmitters in ALLOWED_TRANSMITTERS - nothing will be accepted
Keep that address. The warning is expected until step 5.
The radio hop is encrypted with two 16-character keys, one shared by the
link and one per pair of boards. Generate two random ones and paste them
into both config.h files, identical:
python -c "import secrets; print(secrets.token_urlsafe(12)[:16])" python -c "import secrets; print(secrets.token_urlsafe(12)[:16])"
Sixteen characters exactly; the sketch refuses to compile otherwise.
The dongle to the camera board:
| Dongle | ESP32-CAM |
|---|---|
| 5V | 5V |
| GND | GND |
| TXD | U0R |
| RXD | U0T |
Connect IO0 to GND to enter the bootloader. Only while programming.
Copy transmitter/config.example.h to transmitter/config.h.
Fill in the two keys and the receiver address from step 2 as
{0xAC, 0x27, 0x6E, 0xA4, 0xD3, 0x38}. Board: AI Thinker
ESP32-CAM, or ESP32 Dev Module with PSRAM: Enabled and
partition scheme Huge APP. Upload at 115200.
Remove the IO0 jumper, unplug and replug the dongle, open the Serial Monitor at 115200:
Transmitter MAC: 24:6F:28:AA:BB:CC Camera initialized Link: encrypted (PMK+LMK) Transmitter ready.
Keep that address too.
config.h files or
not at all.Put the camera's address into receiver/config.h:
#define ALLOWED_TRANSMITTERS { \
{0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC}, \
}
Up to six cameras per receiver. Upload the receiver again. Its console now
prints Allowed transmitter: 24:6F:28:AA:BB:CC, and once the
camera boots you will see Image transmission successful. on the
camera's console every few seconds.
Plug the receiver into the Pi. Find its port with
ls /dev/ttyUSB* /dev/ttyACM*, unplug, list again; the one that
vanished is it. Then:
pip install pyserial python3 espnow_bridge.py --port /dev/ttyUSB0 --out camera
It prints one line per saved frame and keeps camera/latest.jpg
fresh. A frame that fails its length or checksum check is rejected, and a
frame the camera gave up on is abandoned the moment the next one starts.
The node reader accepts an image path and re-reads it every time the file changes, so nothing else is special about this camera:
python3 detect/run_live.py --source camera/latest.jpg
Enrol the node the same way as any other camera on the contribute page. Aim it at a public road from your own property, like every node on the map.
| You see | It means |
|---|---|
Retry start (attempt 8) then FAILED at frame start |
The receiver is not answering. Wrong receiver address in the camera's config, the camera is not in the receiver's list, the keys differ, or the channels differ. |
Camera init failed | Ribbon cable not seated, or the wrong board selected. Reseat the lens ribbon and pick the AI Thinker board. |
WARNING: PSRAM not found | PSRAM disabled in the board settings. It falls back to QVGA, which works but reads plates less far. |
| Garbage in the receiver's serial monitor | Wrong baud. It is 921600 on the receiver and 115200 on the camera. |
rejected frame N: crc ... on the Pi | A chunk arrived corrupted. Rare; the next frame will be fine. Constant rejections mean interference on the channel. |
| Frames arrive but nothing reaches the map | The node is not running or points at the wrong path. latest.jpg must be the file the bridge is writing. |
If you build one, tell us on the contribute page, and if you improve it, open a pull request. Lucian's original three files are kept unchanged in the repo beside the reviewed ones, so you can always see exactly what came from him.