This commit is contained in:
Joshua King
2026-10-02 12:23:58 -04:00
parent f0af1dbbcb
commit ad1e7e6d01
219 changed files with 488314 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# DFR1154 camera bring-up
The sketch uses the pin mapping from [DFRobot's DFR1154 CameraWebServer example](https://github.com/DFRobot/DFR1154_Examples/blob/master/5.3%20CameraWebServer/source%20code/5_3/5_3.ino), initializes the OV3660 at QVGA JPEG, and sends one frame over USB serial when the host sends `C`. It avoids Wi-Fi setup and stores no credentials. `capture_frame.py` validates and saves the JPEG inside `hardware/camera-captures/`.
The board under test is marked **V1.1**. Its ESP32-S3 MAC is `E8:F6:0A:87:0B:4C`, with 16 MB flash and 8 MB PSRAM. The USB port was `/dev/cu.usbmodem1101` during initial inspection; verify it before uploading because port names can change. Only upload after checking the board MAC. Flashing will replace its installed program.
Compile profile: `esp32:esp32:esp32s3:FlashSize=16M,PSRAM=opi,PartitionScheme=app3M_fat9M_16MB,USBMode=hwcdc,CDCOnBoot=cdc` using Arduino ESP32 core 3.3.5. The existing `vendor/ctags-tool/bin` arm64 helper is required on this Mac. The host capture script can run with `/Users/joshua/.local/pipx/venvs/esptool/bin/python`, which has PySerial.
The first successful result should include a readable frame with reasonable exposure and no severe corruption. Save a serial log and note its actual dimensions, lighting, and camera orientation in `hardware/bringup.md`. This test proves image capture, not person localization or tracking speed.
@@ -0,0 +1,95 @@
// DFRobot DFR1154 camera bring-up: capture one QVGA JPEG on each 'C' command.
// GPIO assignments match DFRobot's DFR1154 CameraWebServer example.
// The serial protocol contains no Wi-Fi credentials or network dependency.
#include <Arduino.h>
#include "esp_camera.h"
namespace {
bool camera_ready = false;
void print_error(const char* stage, esp_err_t error) {
Serial.printf("ERROR %s 0x%X\n", stage, static_cast<unsigned int>(error));
}
void capture_one_frame() {
if (!camera_ready) {
Serial.println("ERROR camera_not_ready");
return;
}
camera_fb_t* frame = esp_camera_fb_get();
if (!frame) {
Serial.println("ERROR capture_failed");
return;
}
if (frame->format != PIXFORMAT_JPEG || frame->len == 0) {
Serial.println("ERROR unexpected_frame_format");
esp_camera_fb_return(frame);
return;
}
Serial.printf("JPEG %u %u %u\n", static_cast<unsigned int>(frame->len),
static_cast<unsigned int>(frame->width),
static_cast<unsigned int>(frame->height));
Serial.write(frame->buf, frame->len);
Serial.print("\nDONE\n");
Serial.flush();
esp_camera_fb_return(frame);
}
} // namespace
void setup() {
Serial.begin(115200);
delay(500);
camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 16;
config.pin_d1 = 18;
config.pin_d2 = 21;
config.pin_d3 = 17;
config.pin_d4 = 14;
config.pin_d5 = 7;
config.pin_d6 = 6;
config.pin_d7 = 4;
config.pin_xclk = 5;
config.pin_pclk = 15;
config.pin_vsync = 1;
config.pin_href = 2;
config.pin_sccb_sda = 8;
config.pin_sccb_scl = 9;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
if (!psramFound()) {
Serial.println("ERROR psram_missing");
return;
}
const esp_err_t result = esp_camera_init(&config);
if (result != ESP_OK) {
print_error("camera_init", result);
return;
}
sensor_t* sensor = esp_camera_sensor_get();
if (sensor) {
Serial.printf("SENSOR_PID 0x%04X\n", sensor->id.PID);
if (sensor->id.PID == OV3660_PID) sensor->set_vflip(sensor, 1);
}
camera_ready = true;
Serial.println("CAMERA_READY QVGA_JPEG");
}
void loop() {
while (Serial.available()) {
if (Serial.read() == 'C') capture_one_frame();
}
delay(5);
}
+74
View File
@@ -0,0 +1,74 @@
"""Request one JPEG from camera_bringup.ino and keep evidence in this project."""
from pathlib import Path
import argparse
import hashlib
import time
import serial
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUTPUT = PROJECT_ROOT / "hardware" / "camera-captures" / "first-frame.jpg"
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--port", required=True)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
serial_lines = []
with serial.Serial(args.port, 115200, timeout=2, write_timeout=2) as camera:
time.sleep(1)
camera.reset_input_buffer()
camera.write(b"C")
deadline = time.monotonic() + 12
header = None
while time.monotonic() < deadline:
line = camera.readline().decode("ascii", errors="replace").strip()
if line:
print(line)
serial_lines.append(line)
if line.startswith("ERROR "):
raise RuntimeError(line)
if line.startswith("JPEG "):
header = line
break
if header is None:
raise TimeoutError("Camera did not return a JPEG header")
_, size_text, width_text, height_text = header.split()
size = int(size_text)
if not 100 <= size <= 500_000:
raise ValueError(f"Unexpected JPEG length: {size}")
data = camera.read(size)
if len(data) != size:
raise IOError(f"Incomplete JPEG: {len(data)} of {size} bytes")
if not (data.startswith(b"\xff\xd8") and data.endswith(b"\xff\xd9")):
raise ValueError("JPEG markers missing")
trailer = camera.readline().strip()
if not trailer:
trailer = camera.readline().strip()
if trailer != b"DONE":
raise ValueError(f"Unexpected frame trailer: {trailer!r}")
serial_lines.append("DONE")
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(data)
log_path = args.output.with_suffix(".log")
log_path.write_text(
"\n".join(
[
f"port={args.port}",
f"jpeg_sha256={hashlib.sha256(data).hexdigest()}",
*serial_lines,
]
) + "\n"
)
print(f"Saved {width_text}x{height_text} JPEG ({size} bytes): {args.output}")
print(f"Saved serial capture log: {log_path}")
if __name__ == "__main__":
main()