← all guides

An ESP32-CAM with no network

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.

Designed and built by Lucian H. This is the first SparrowMap camera not built by the project's author. His own repo with the build photos is lucianbuilds/espnow-image-bridge; the SparrowMap copy with the encryption and Pi-side changes described below is firmware/esp32cam-espnow.
What it is good forA gate, a checkpoint, a road end, a place a patrol parks. Anywhere you cannot run a wire or reach WiFi. Range is set by the receiver's antenna, not the camera.
What it is notA detector feed. A VGA still is about 150 radio packets and takes one to two seconds to cross, plus the capture interval, so expect one frame every 6 to 7 seconds. A car passes in one or two. Think snapshot sentinel, not traffic camera.
Nothing leaves readableThe radio hop is encrypted with keys you generate once. The Pi crops and blurs exactly as every other node does, and no video exists anywhere in the chain.

What to buy

PartWhich oneWhy
CameraAI-Thinker ESP32-CAM with the OV2640 lens. Lucian used the Hosyond kit. The board's PSRAM is what allows VGA stills.
USB-TTL dongleA CP2102 adapter, e.g. this one. Some kits include one.The camera board has no USB port of its own.
ReceiverAny 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.
HostA Raspberry Pi, or any computer with a USB port. Runs the bridge script and the SparrowMap node.

Set it up

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.

1 Arduino IDE

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.

2 Flash the receiver, copy its address

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.

3 Make the two keys

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.

Why this is not optional: the picture on the air is a full still of a public road with every plate readable. Without the keys, anyone in 2.4 GHz range could reassemble it, and the receiver would accept pictures from any board that learned the packet format. With them, a board without the keys decrypts to noise and is dropped, and a board not on the receiver's list is dropped before it is even read.

4 Wire and flash the camera

The dongle to the camera board:

DongleESP32-CAM
5V5V
GNDGND
TXDU0R
RXDU0T

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.

Channel 6 is the default on both boards. In the US, 1, 6 and 11 are the clean choices. Change it in both config.h files or not at all.

5 Tell the receiver which cameras to trust

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.

6 The Pi rebuilds the pictures

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.

7 Point SparrowMap at it

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.

If it does not work

You seeIt 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 failedRibbon cable not seated, or the wrong board selected. Reseat the lens ribbon and pick the AI Thinker board.
WARNING: PSRAM not foundPSRAM disabled in the board settings. It falls back to QVGA, which works but reads plates less far.
Garbage in the receiver's serial monitorWrong baud. It is 921600 on the receiver and 115200 on the camera.
rejected frame N: crc ... on the PiA chunk arrived corrupted. Rare; the next frame will be fine. Constant rejections mean interference on the channel.
Frames arrive but nothing reaches the mapThe 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.