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
Vendored
BIN
View File
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
# Downloaded vendor examples and local build artifacts are reproducible.
vendor/ESP32-S3-Touch-LCD-2.1-Demo.zip
vendor/extracted/
vendor/build/
vendor/build-attempt.log
vendor/ctags-5.8-arduino11.zip
vendor/ctags-5.8-arduino11/
vendor/ctags-tool/
firmware/backups/*.bin
firmware/build-eye-test/
firmware/build-eye-test.log
firmware/build-eye-test-board2/
firmware/build-eye-test-board2.log
firmware/build-tf-luna-bench/
firmware/build-tf-luna-bench.log
firmware/build-benewake-range-bench/
firmware/build-benewake-range-bench.log
firmware/build-camera-bringup/
firmware/build-camera-bringup.log
firmware/camera_person_idf/build/
firmware/camera_person_idf/managed_components/
firmware/camera_person_idf/sdkconfig
vendor/Person_Detection_inferencing.zip
__pycache__/
firmware/private/
+61
View File
@@ -0,0 +1,61 @@
# Freddy Fazbear digital eyes
Two animated round LCD eyes for a stationary Freddy Fazbear mask on a covered porch beside the front door. The eyes should blink and move naturally, then follow people walking up to the house. The ESP32 eye boards remain the display controllers. Joshua is now evaluating his Raspberry Pi Zero 2 W with a Pi camera as the person-detection and coordination node; the existing DFRobot ESP32 camera remains a working bench baseline.
## Hardware and current state
- **Eyes:** Two Waveshare ESP32-S3-Touch-LCD-2.1 modules, 480 × 480 pixels, with their own ESP32-S3 controllers. Both respond over USB as distinct ESP32-S3 devices with 8 MB PSRAM and 16 MB flash. Both arrived with an “Onboard parameters” demo and working touch, and now run the eye test. Their backs are marked Rev2.0, and Waveshare maps the printed model to SKU 28169. See [hardware/bringup.md](hardware/bringup.md).
- **Firmware baseline:** The official Waveshare Arduino/LVGL display example compiles on this laptop with the board's memory settings. A custom eye test runs on both boards with distinct on-screen labels. Both boards have been flashed and hash-verified with the supplied Freddy iris texture and matching `#743824` eyelids/perimeter rings. Joshua visually confirmed both displays look around and blink with the matching design. Frame-rate measurement and post-flash image evidence remain. See [firmware/README.md](firmware/README.md).
- **Wi-Fi updates:** Both eyes and the camera have joined the home Wi-Fi and each completed a verified OTA application update on 2026-09-30. The camera's one-time dual-slot partition migration is complete. After the updates, the camera still detected people and Board 2 received its wired gaze packets; Board 1 resumed receiving them after Joshua fixed its cable. See [firmware/ota.md](firmware/ota.md).
- **Vision:** The DFRobot DFR1154 V1.1 ESP32-S3 camera is the current bench source and produces person-position results at about four updates per second. On 2026-10-01 its USB-flashed tracker added a one-second hold for brief misses and rejected a measured empty-scene box artifact. In a 20-second empty-view retest, it sent 0 valid targets in 80 frames; real-person performance with this revision is still unverified. Joshua wants to test his Pi Zero 2 W as the eventual vision source while retaining the ESP32 eyes. He identified an owned InnoMaker OV5647 camera with an IR-filter lens and showed two Arducam alternatives. His preferred first placement trial is the Arducam OV5647 day/night camera and its IR emitters **inside Freddy's mouth**. See [the mouth placement trial](planning/mouth-camera.md), [Pi vision plan](planning/pi-vision.md), and [outdoor deployment plan](planning/outdoor-deployment.md).
- **Gaze link:** The camera's versioned 3.3 V UART broadcast was bench-verified with both eyes: they accepted identical coordinates, visibly tracked together, rejected malformed and stale frames, then recovered. After the 2026-09-30 OTA updates, Board 2 kept receiving gaze packets; Board 1 resumed receiving them after its cable was fixed, accepting 48 packets in a 12-second check. Final-position calibration, active-tracking signal-loss behavior, and walk-by testing remain. See [protocol/gaze.md](protocol/gaze.md) and [the bench log](hardware/gaze-link-bench.md).
- **Tracking effect:** Both eyes now switch to the glowing-red look when the camera sends a confirmed person target (`TRACK`) and return to blue on an invalid target or 750 ms gaze-signal timeout (`IDLE`). The former touch-toggle preview was removed. Both eye updates were delivered over Wi-Fi on 2026-10-01; Joshua confirmed the red-to-blue transition and later reported the eyes appearing synchronized. A distance sensor is no longer needed for this effect. See [planning/proximity-red.md](planning/proximity-red.md).
- **Mask:** The [Shapr3D mask STEP](stls/freddy_fazbear_mask.step) has been measured, and an official Waveshare board CAD reference inspected separately. Each straight-on projected eye opening is about 62.5 mm wide by 54.5 mm high; the largest fully clear circle is about 52.2 mm across. These are CAD sightline measurements, not physical-print tolerances. [Connected rear](reference_images/IMG_6530.jpeg) and [powered front](reference_images/IMG_6535.jpeg) dry-fit photos show one board behind an opening. A [first-fit removable mount set](hardware/eye_mount/README.md) now has candidate carriers, shell pads, stand-off sleeves, and a trim appearance gauge; the CAD clears the mask and vendor board reference but needs physical fit testing with both plugged boards. Joshua wants an integrated look: a retained, mask-finished surround should conceal the exposed dark socket and display bezel without touching the glass. Final centering/rotation, both-eye physical fit, cable strain relief, and repeatable mask attachment remain unverified. See [hardware/fit.md](hardware/fit.md).
- **Project records:** Maestro project `Freddy Fazbear Digital Eyes` in the `Maniacs-Mansion` workspace tracks tasks. CTXD project `freddy-fazbear-digital-eyes` records durable context and decisions.
## Proposed system
```text
Vision controller: current DFRobot ESP32-S3; proposed Pi Zero 2 W + Pi camera
detects a person and chooses one target
sends normalized target position + validity
|
3.3 V serial link
|
+---------+---------+
| |
left Waveshare ESP32 right Waveshare ESP32
eye animation + LCD eye animation + LCD
```
Each eye renders locally, so blinking and smooth movement continue between camera updates. A wired UART broadcast to both eye receivers is bench-verified. The proposed Pi would send the same frame format over one 3.3 V serial output, replacing the DFRobot camera's TX connection. Share signal ground and power each board from an appropriately sized supply rather than another board's 3.3 V output. The vision controller must provide a person's **position** (bounding-box center), not only a yes/no detection.
## Build sequence
1. **Bring up and measure hardware.** Identify board revisions, run the vendor display demo on each board, locate and test the camera, measure eye clearance and cable paths, and choose the power scheme.
2. **Create the eye renderer.** On one board, draw a full-size eyeball with movable iris/pupil and eyelids. Add natural blink timing, eased gaze motion, idle behavior, and a configurable left/right orientation. Run it on both boards before adding tracking.
3. **Prove camera localization.** Get live camera frames and a model that returns person coordinates. Select one target consistently; handle crossings and temporary loss. Measure detection rate and latency in the intended lighting.
4. **Connect tracking to the eyes.** Define a small versioned gaze message, verify serial delivery to both boards, calibrate camera coordinates to eye motion, and measure end-to-end response. Both boards must continue idle animation if tracking or communication stops.
5. **Install and validate outdoors.** Design removable mounts, a camera sightline, and weather protection for the electronics. Test moving pedestrians in daylight, shade, dusk, and darkness from the actual path. Verify the installed power, ventilation, repeated tracking, and a sustained run.
## Acceptance checks
- Both displays boot reliably and show independently calibrated eyes.
- Each eye moves and blinks smoothly with no camera connected.
- The camera reports the location of a person walking across its field of view; the eyes visibly follow that person without jumping unpredictably between targets.
- Tracking loss returns the eyes to an idle state; recovery resumes following without a reboot.
- The installed electronics fit without forcing the display, bending its flex cable sharply, or obstructing the camera.
- The protected outdoor installation keeps the lens clear of rain, condensation, and IR reflections while capturing the full approach path.
- Walk-by tests at the installed distance show prompt, stable following in the expected daylight and night lighting; empty-path tests do not trigger false gaze.
- A 30-minute powered run completes without a reset, visible display corruption, or unsafe heat buildup.
## Decisions still to make
- Exact camera position and distance to people approaching the covered front porch.
- Final camera placement, outdoor person-detection performance target, weather enclosure, and power source.
- Exact eye look (iris color/texture, eyelid shape, and any character-specific expression).
## References
- [Waveshare ESP32-S3-Touch-LCD-2.1 documentation](https://docs.waveshare.com/ESP32-S3-Touch-LCD-2.1)
- [DFRobot DFR1154 documentation](https://wiki.dfrobot.com/dfr1154/)
+14
View File
@@ -0,0 +1,14 @@
# Freddy eye appearance baseline
Established on 2026-09-21 from Joshua's supplied [`official_freddy_eye_texture.webp`](../reference_images/official_freddy_eye_texture.webp). Both boards show the texture moving and blinking correctly; Joshua confirmed their matching brown eyelids and perimeter rings look good. This is the current visual baseline, not a camera-tracking test.
- **Iris and pupil:** Use the supplied blue radial texture, including its black pupil and two white highlights. The original file remains unchanged. The embedded iris is 178 × 178 pixels with a transparent feathered edge.
- **Sclera:** Warm cream `#F5F4EA`, 432-pixel circle on the 480 × 480 canvas.
- **Outer eye ring:** Brown `#743824`, matching the lids; 444-pixel circle.
- **Lids:** Brown `#743824`, chosen by Joshua after the first textured-eye test. The current blink is a straight upper/lower closure lasting 300 ms every 4.6 seconds; this eyelid shape can be refined for the final character expression.
- **Surrounding background:** Dark brown `#39271D`.
- **Brightness:** The Waveshare driver initializes its backlight to 50 on its 0–100 setting. This is the tested bench setting, not a measured luminance value.
[Display composition preview](../firmware/assets/freddy_eye_display_preview.png) shows the centered open-eye layout without the small board-test label. The [iris-only preview](../firmware/assets/freddy_iris_preview.png) shows the encoded image and transparent edge. The physical display was visually confirmed by Joshua; a screen photo and frame-rate measurement remain to be recorded.
[Mid-blink preview](../firmware/assets/freddy_eye_blink_preview.png) shows the new eyelid color. The straight-edge lids are an early animation baseline rather than sculpted eyelids.
+13
View File
@@ -0,0 +1,13 @@
#!/bin/zsh
# Double-click in Finder to configure this project's Wi-Fi without sharing a password in chat.
cd -- "${0:A:h}" || exit 1
python3 firmware/tools/configure_ota.py
result=$?
echo
if (( result == 0 )); then
echo "Wi-Fi configuration saved. You may close this window."
else
echo "Configuration was not saved. You may close this window and retry."
fi
read -k 1 "?Press any key to close..."
exit $result
@@ -0,0 +1,63 @@
{"time": "2026-10-01T20:08:51.860401-04:00", "frame": 1, "detect_ms": 187.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 0}}
{"time": "2026-10-01T20:08:52.027690-04:00", "frame": 2, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 1}}
{"time": "2026-10-01T20:08:52.187516-04:00", "frame": 3, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 2}}
{"time": "2026-10-01T20:08:52.367121-04:00", "frame": 4, "detect_ms": 176.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 3}}
{"time": "2026-10-01T20:08:52.527115-04:00", "frame": 5, "detect_ms": 157.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 4}}
{"time": "2026-10-01T20:08:52.686809-04:00", "frame": 6, "detect_ms": 157.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 5}}
{"time": "2026-10-01T20:08:52.846740-04:00", "frame": 7, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 6}}
{"time": "2026-10-01T20:08:53.007223-04:00", "frame": 8, "detect_ms": 157.5, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 7}}
{"time": "2026-10-01T20:08:53.167244-04:00", "frame": 9, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 8}}
{"time": "2026-10-01T20:08:53.326911-04:00", "frame": 10, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 9}}
{"time": "2026-10-01T20:08:53.487192-04:00", "frame": 11, "detect_ms": 157.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 10}}
{"time": "2026-10-01T20:08:53.647092-04:00", "frame": 12, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 11}}
{"time": "2026-10-01T20:08:53.807359-04:00", "frame": 13, "detect_ms": 157.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 12}}
{"time": "2026-10-01T20:08:53.968262-04:00", "frame": 14, "detect_ms": 158.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 13}}
{"time": "2026-10-01T20:08:54.128034-04:00", "frame": 15, "detect_ms": 157.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 14}}
{"time": "2026-10-01T20:08:54.287632-04:00", "frame": 16, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 15}}
{"time": "2026-10-01T20:08:54.447454-04:00", "frame": 17, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 16}}
{"time": "2026-10-01T20:08:54.607370-04:00", "frame": 18, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 17}}
{"time": "2026-10-01T20:08:54.768511-04:00", "frame": 19, "detect_ms": 158.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 18}}
{"time": "2026-10-01T20:08:54.928200-04:00", "frame": 20, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 19}}
{"time": "2026-10-01T20:08:55.088005-04:00", "frame": 21, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 20}}
{"time": "2026-10-01T20:08:55.247575-04:00", "frame": 22, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 21}}
{"time": "2026-10-01T20:08:55.407955-04:00", "frame": 23, "detect_ms": 157.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 22}}
{"time": "2026-10-01T20:08:55.568485-04:00", "frame": 24, "detect_ms": 157.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 23}}
{"time": "2026-10-01T20:08:55.728131-04:00", "frame": 25, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 24}}
{"time": "2026-10-01T20:08:55.887710-04:00", "frame": 26, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 25}}
{"time": "2026-10-01T20:08:56.047685-04:00", "frame": 27, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 26}}
{"time": "2026-10-01T20:08:56.207662-04:00", "frame": 28, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 27}}
{"time": "2026-10-01T20:08:56.368243-04:00", "frame": 29, "detect_ms": 157.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 28}}
{"time": "2026-10-01T20:08:56.528154-04:00", "frame": 30, "detect_ms": 157.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 29}}
{"time": "2026-10-01T20:08:56.687829-04:00", "frame": 31, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 30}}
{"time": "2026-10-01T20:08:56.847369-04:00", "frame": 32, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 31}}
{"time": "2026-10-01T20:08:57.007629-04:00", "frame": 33, "detect_ms": 157.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 32}}
{"time": "2026-10-01T20:08:57.168355-04:00", "frame": 34, "detect_ms": 157.5, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 33}}
{"time": "2026-10-01T20:08:57.328697-04:00", "frame": 35, "detect_ms": 157.6, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 34}}
{"time": "2026-10-01T20:08:57.488258-04:00", "frame": 36, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 35}}
{"time": "2026-10-01T20:08:57.648239-04:00", "frame": 37, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 36}}
{"time": "2026-10-01T20:08:57.808977-04:00", "frame": 38, "detect_ms": 158.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 37}}
{"time": "2026-10-01T20:08:57.970469-04:00", "frame": 39, "detect_ms": 158.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 38}}
{"time": "2026-10-01T20:08:58.131296-04:00", "frame": 40, "detect_ms": 158.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 39}}
{"time": "2026-10-01T20:08:58.291602-04:00", "frame": 41, "detect_ms": 157.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 40}}
{"time": "2026-10-01T20:08:58.452280-04:00", "frame": 42, "detect_ms": 158.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 41}}
{"time": "2026-10-01T20:08:58.612161-04:00", "frame": 43, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 42}}
{"time": "2026-10-01T20:08:58.773113-04:00", "frame": 44, "detect_ms": 158.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 43}}
{"time": "2026-10-01T20:08:58.933739-04:00", "frame": 45, "detect_ms": 157.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 44}}
{"time": "2026-10-01T20:08:59.093384-04:00", "frame": 46, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 45}}
{"time": "2026-10-01T20:08:59.252938-04:00", "frame": 47, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 46}}
{"time": "2026-10-01T20:08:59.412734-04:00", "frame": 48, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 47}}
{"time": "2026-10-01T20:08:59.573255-04:00", "frame": 49, "detect_ms": 157.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 48}}
{"time": "2026-10-01T20:08:59.733840-04:00", "frame": 50, "detect_ms": 157.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 49}}
{"time": "2026-10-01T20:08:59.894475-04:00", "frame": 51, "detect_ms": 157.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 50}}
{"time": "2026-10-01T20:09:00.054185-04:00", "frame": 52, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 51}}
{"time": "2026-10-01T20:09:00.213725-04:00", "frame": 53, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 52}}
{"time": "2026-10-01T20:09:00.374269-04:00", "frame": 54, "detect_ms": 157.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 53}}
{"time": "2026-10-01T20:09:00.535376-04:00", "frame": 55, "detect_ms": 157.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 54}}
{"time": "2026-10-01T20:09:00.695952-04:00", "frame": 56, "detect_ms": 157.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 55}}
{"time": "2026-10-01T20:09:00.855738-04:00", "frame": 57, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 56}}
{"time": "2026-10-01T20:09:01.015534-04:00", "frame": 58, "detect_ms": 157.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 57}}
{"time": "2026-10-01T20:09:01.175303-04:00", "frame": 59, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 58}}
{"time": "2026-10-01T20:09:01.335585-04:00", "frame": 60, "detect_ms": 157.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 59}}
{"time": "2026-10-01T20:09:01.496998-04:00", "frame": 61, "detect_ms": 158.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 60}}
{"time": "2026-10-01T20:09:01.656972-04:00", "frame": 62, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 61}}
{"time": "2026-10-01T20:09:01.816924-04:00", "frame": 63, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 62}}
@@ -0,0 +1,157 @@
{"time": "2026-10-01T20:10:07.792286-04:00", "frame": 1, "detect_ms": 160.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 0}}
{"time": "2026-10-01T20:10:07.952419-04:00", "frame": 2, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 1}}
{"time": "2026-10-01T20:10:08.111679-04:00", "frame": 3, "detect_ms": 156.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 2}}
{"time": "2026-10-01T20:10:08.271285-04:00", "frame": 4, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 3}}
{"time": "2026-10-01T20:10:08.430363-04:00", "frame": 5, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 4}}
{"time": "2026-10-01T20:10:08.589691-04:00", "frame": 6, "detect_ms": 156.5, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 5}}
{"time": "2026-10-01T20:10:08.748481-04:00", "frame": 7, "detect_ms": 155.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 6}}
{"time": "2026-10-01T20:10:08.907515-04:00", "frame": 8, "detect_ms": 156.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 7}}
{"time": "2026-10-01T20:10:09.066705-04:00", "frame": 9, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 8}}
{"time": "2026-10-01T20:10:09.226771-04:00", "frame": 10, "detect_ms": 156.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 9}}
{"time": "2026-10-01T20:10:09.387280-04:00", "frame": 11, "detect_ms": 157.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 10}}
{"time": "2026-10-01T20:10:09.545796-04:00", "frame": 12, "detect_ms": 155.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 11}}
{"time": "2026-10-01T20:10:09.704561-04:00", "frame": 13, "detect_ms": 156.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 12}}
{"time": "2026-10-01T20:10:09.864256-04:00", "frame": 14, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 13}}
{"time": "2026-10-01T20:10:10.023478-04:00", "frame": 15, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 14}}
{"time": "2026-10-01T20:10:10.182799-04:00", "frame": 16, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 15}}
{"time": "2026-10-01T20:10:10.341514-04:00", "frame": 17, "detect_ms": 156.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 16}}
{"time": "2026-10-01T20:10:10.501279-04:00", "frame": 18, "detect_ms": 156.5, "target": {"x": 19, "y": 71, "width": 85, "height": 169, "score": 0.5118925448041409}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 17}}
{"time": "2026-10-01T20:10:10.661254-04:00", "frame": 19, "detect_ms": 156.8, "target": {"x": 29, "y": 77, "width": 78, "height": 156, "score": 0.7669728476321325}, "gaze": {"x": 210, "y": 644, "valid": true, "sequence": 18}}
{"time": "2026-10-01T20:10:10.820317-04:00", "frame": 20, "detect_ms": 156.3, "target": null, "gaze": {"x": 210, "y": 644, "valid": true, "sequence": 19}}
{"time": "2026-10-01T20:10:10.980518-04:00", "frame": 21, "detect_ms": 157.0, "target": {"x": 56, "y": 76, "width": 82, "height": 164, "score": 0.20176295610144734}, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 20}}
{"time": "2026-10-01T20:10:11.139645-04:00", "frame": 22, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 21}}
{"time": "2026-10-01T20:10:11.298981-04:00", "frame": 23, "detect_ms": 156.6, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 22}}
{"time": "2026-10-01T20:10:11.458946-04:00", "frame": 24, "detect_ms": 156.9, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 23}}
{"time": "2026-10-01T20:10:11.618174-04:00", "frame": 25, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 24}}
{"time": "2026-10-01T20:10:11.777165-04:00", "frame": 26, "detect_ms": 156.2, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 25}}
{"time": "2026-10-01T20:10:11.937112-04:00", "frame": 27, "detect_ms": 157.2, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 26}}
{"time": "2026-10-01T20:10:12.096323-04:00", "frame": 28, "detect_ms": 156.6, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 27}}
{"time": "2026-10-01T20:10:12.255717-04:00", "frame": 29, "detect_ms": 156.6, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 28}}
{"time": "2026-10-01T20:10:12.415172-04:00", "frame": 30, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 29}}
{"time": "2026-10-01T20:10:12.574233-04:00", "frame": 31, "detect_ms": 156.1, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 30}}
{"time": "2026-10-01T20:10:12.733777-04:00", "frame": 32, "detect_ms": 156.9, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 31}}
{"time": "2026-10-01T20:10:12.894184-04:00", "frame": 33, "detect_ms": 157.6, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 32}}
{"time": "2026-10-01T20:10:13.053713-04:00", "frame": 34, "detect_ms": 156.6, "target": {"x": 85, "y": 85, "width": 76, "height": 152, "score": 0.4825885985046625}, "gaze": {"x": 382, "y": 669, "valid": true, "sequence": 33}}
{"time": "2026-10-01T20:10:13.212758-04:00", "frame": 35, "detect_ms": 156.2, "target": null, "gaze": {"x": 382, "y": 669, "valid": true, "sequence": 34}}
{"time": "2026-10-01T20:10:13.371814-04:00", "frame": 36, "detect_ms": 156.2, "target": null, "gaze": {"x": 382, "y": 669, "valid": true, "sequence": 35}}
{"time": "2026-10-01T20:10:13.531051-04:00", "frame": 37, "detect_ms": 156.5, "target": null, "gaze": {"x": 382, "y": 669, "valid": true, "sequence": 36}}
{"time": "2026-10-01T20:10:13.690939-04:00", "frame": 38, "detect_ms": 156.6, "target": {"x": 38, "y": 70, "width": 86, "height": 170, "score": 0.4403667140286416}, "gaze": {"x": 250, "y": 644, "valid": true, "sequence": 37}}
{"time": "2026-10-01T20:10:13.850237-04:00", "frame": 39, "detect_ms": 156.4, "target": {"x": 28, "y": 72, "width": 83, "height": 166, "score": 1.2733431142987683}, "gaze": {"x": 216, "y": 644, "valid": true, "sequence": 38}}
{"time": "2026-10-01T20:10:14.009192-04:00", "frame": 40, "detect_ms": 156.0, "target": {"x": 20, "y": 74, "width": 80, "height": 160, "score": 0.13930590322706848}, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 39}}
{"time": "2026-10-01T20:10:14.168612-04:00", "frame": 41, "detect_ms": 156.6, "target": null, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 40}}
{"time": "2026-10-01T20:10:14.328067-04:00", "frame": 42, "detect_ms": 156.5, "target": null, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 41}}
{"time": "2026-10-01T20:10:14.487545-04:00", "frame": 43, "detect_ms": 156.7, "target": null, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 42}}
{"time": "2026-10-01T20:10:14.646794-04:00", "frame": 44, "detect_ms": 156.3, "target": null, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 43}}
{"time": "2026-10-01T20:10:14.805921-04:00", "frame": 45, "detect_ms": 156.4, "target": null, "gaze": {"x": 184, "y": 640, "valid": true, "sequence": 44}}
{"time": "2026-10-01T20:10:14.965771-04:00", "frame": 46, "detect_ms": 156.6, "target": {"x": 0, "y": 85, "width": 70, "height": 155, "score": 0.41224189777858555}, "gaze": {"x": 106, "y": 677, "valid": true, "sequence": 45}}
{"time": "2026-10-01T20:10:15.125518-04:00", "frame": 47, "detect_ms": 156.9, "target": null, "gaze": {"x": 106, "y": 677, "valid": true, "sequence": 46}}
{"time": "2026-10-01T20:10:15.284509-04:00", "frame": 48, "detect_ms": 156.2, "target": null, "gaze": {"x": 106, "y": 677, "valid": true, "sequence": 47}}
{"time": "2026-10-01T20:10:15.443257-04:00", "frame": 49, "detect_ms": 156.0, "target": null, "gaze": {"x": 106, "y": 677, "valid": true, "sequence": 48}}
{"time": "2026-10-01T20:10:15.601973-04:00", "frame": 50, "detect_ms": 155.9, "target": {"x": 28, "y": 80, "width": 76, "height": 152, "score": 0.37580482847988605}, "gaze": {"x": 203, "y": 648, "valid": true, "sequence": 49}}
{"time": "2026-10-01T20:10:15.761940-04:00", "frame": 51, "detect_ms": 156.8, "target": {"x": 50, "y": 74, "width": 80, "height": 160, "score": 0.199772342806682}, "gaze": {"x": 278, "y": 640, "valid": true, "sequence": 50}}
{"time": "2026-10-01T20:10:15.921655-04:00", "frame": 52, "detect_ms": 156.8, "target": {"x": 60, "y": 78, "width": 74, "height": 148, "score": 0.16141578240785748}, "gaze": {"x": 300, "y": 631, "valid": true, "sequence": 51}}
{"time": "2026-10-01T20:10:16.080652-04:00", "frame": 53, "detect_ms": 156.1, "target": {"x": 74, "y": 82, "width": 69, "height": 138, "score": 0.07168729533441365}, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 52}}
{"time": "2026-10-01T20:10:16.239518-04:00", "frame": 54, "detect_ms": 156.1, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 53}}
{"time": "2026-10-01T20:10:16.399060-04:00", "frame": 55, "detect_ms": 156.9, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 54}}
{"time": "2026-10-01T20:10:16.558732-04:00", "frame": 56, "detect_ms": 156.8, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 55}}
{"time": "2026-10-01T20:10:16.718005-04:00", "frame": 57, "detect_ms": 156.5, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 56}}
{"time": "2026-10-01T20:10:16.876777-04:00", "frame": 58, "detect_ms": 156.0, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 57}}
{"time": "2026-10-01T20:10:17.035845-04:00", "frame": 59, "detect_ms": 156.4, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 58}}
{"time": "2026-10-01T20:10:17.195032-04:00", "frame": 60, "detect_ms": 156.5, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 59}}
{"time": "2026-10-01T20:10:17.355053-04:00", "frame": 61, "detect_ms": 156.9, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 60}}
{"time": "2026-10-01T20:10:17.513794-04:00", "frame": 62, "detect_ms": 155.9, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 61}}
{"time": "2026-10-01T20:10:17.672914-04:00", "frame": 63, "detect_ms": 156.4, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 62}}
{"time": "2026-10-01T20:10:17.832242-04:00", "frame": 64, "detect_ms": 156.7, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 63}}
{"time": "2026-10-01T20:10:17.992262-04:00", "frame": 65, "detect_ms": 157.3, "target": null, "gaze": {"x": 338, "y": 627, "valid": true, "sequence": 64}}
{"time": "2026-10-01T20:10:18.151768-04:00", "frame": 66, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 65}}
{"time": "2026-10-01T20:10:18.310645-04:00", "frame": 67, "detect_ms": 155.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 66}}
{"time": "2026-10-01T20:10:18.469751-04:00", "frame": 68, "detect_ms": 156.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 67}}
{"time": "2026-10-01T20:10:18.629214-04:00", "frame": 69, "detect_ms": 156.6, "target": {"x": 56, "y": 76, "width": 82, "height": 164, "score": 0.6699664338957518}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 68}}
{"time": "2026-10-01T20:10:18.789030-04:00", "frame": 70, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 69}}
{"time": "2026-10-01T20:10:18.948384-04:00", "frame": 71, "detect_ms": 156.3, "target": {"x": 32, "y": 73, "width": 84, "height": 167, "score": 0.43034207553137094}, "gaze": {"x": 228, "y": 652, "valid": true, "sequence": 70}}
{"time": "2026-10-01T20:10:19.107178-04:00", "frame": 72, "detect_ms": 155.9, "target": {"x": 24, "y": 71, "width": 85, "height": 169, "score": 0.6316257496364415}, "gaze": {"x": 206, "y": 648, "valid": true, "sequence": 71}}
{"time": "2026-10-01T20:10:19.266604-04:00", "frame": 73, "detect_ms": 156.7, "target": null, "gaze": {"x": 206, "y": 648, "valid": true, "sequence": 72}}
{"time": "2026-10-01T20:10:19.426198-04:00", "frame": 74, "detect_ms": 157.0, "target": null, "gaze": {"x": 206, "y": 648, "valid": true, "sequence": 73}}
{"time": "2026-10-01T20:10:19.585586-04:00", "frame": 75, "detect_ms": 156.5, "target": {"x": 5, "y": 65, "width": 88, "height": 175, "score": 0.2485874259728007}, "gaze": {"x": 150, "y": 635, "valid": true, "sequence": 74}}
{"time": "2026-10-01T20:10:19.744711-04:00", "frame": 76, "detect_ms": 156.2, "target": null, "gaze": {"x": 150, "y": 635, "valid": true, "sequence": 75}}
{"time": "2026-10-01T20:10:19.903986-04:00", "frame": 77, "detect_ms": 156.5, "target": null, "gaze": {"x": 150, "y": 635, "valid": true, "sequence": 76}}
{"time": "2026-10-01T20:10:20.063849-04:00", "frame": 78, "detect_ms": 157.2, "target": null, "gaze": {"x": 150, "y": 635, "valid": true, "sequence": 77}}
{"time": "2026-10-01T20:10:20.223672-04:00", "frame": 79, "detect_ms": 156.9, "target": null, "gaze": {"x": 150, "y": 635, "valid": true, "sequence": 78}}
{"time": "2026-10-01T20:10:20.383218-04:00", "frame": 80, "detect_ms": 156.5, "target": {"x": 24, "y": 77, "width": 82, "height": 163, "score": 0.9722632486373186}, "gaze": {"x": 200, "y": 661, "valid": true, "sequence": 79}}
{"time": "2026-10-01T20:10:20.542442-04:00", "frame": 81, "detect_ms": 156.1, "target": {"x": 36, "y": 76, "width": 82, "height": 164, "score": 0.5289241103455424}, "gaze": {"x": 238, "y": 656, "valid": true, "sequence": 80}}
{"time": "2026-10-01T20:10:20.701880-04:00", "frame": 82, "detect_ms": 156.6, "target": {"x": 54, "y": 78, "width": 78, "height": 156, "score": 0.6763273450196721}, "gaze": {"x": 288, "y": 648, "valid": true, "sequence": 81}}
{"time": "2026-10-01T20:10:20.861573-04:00", "frame": 83, "detect_ms": 156.9, "target": {"x": 56, "y": 76, "width": 82, "height": 164, "score": 0.7153314667521045}, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 82}}
{"time": "2026-10-01T20:10:21.021868-04:00", "frame": 84, "detect_ms": 156.8, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 83}}
{"time": "2026-10-01T20:10:21.180922-04:00", "frame": 85, "detect_ms": 156.3, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 84}}
{"time": "2026-10-01T20:10:21.339810-04:00", "frame": 86, "detect_ms": 156.2, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 85}}
{"time": "2026-10-01T20:10:21.498914-04:00", "frame": 87, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 86}}
{"time": "2026-10-01T20:10:21.658374-04:00", "frame": 88, "detect_ms": 156.8, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 87}}
{"time": "2026-10-01T20:10:21.817776-04:00", "frame": 89, "detect_ms": 156.6, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 88}}
{"time": "2026-10-01T20:10:21.976764-04:00", "frame": 90, "detect_ms": 156.1, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 89}}
{"time": "2026-10-01T20:10:22.136098-04:00", "frame": 91, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 90}}
{"time": "2026-10-01T20:10:22.295226-04:00", "frame": 92, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 91}}
{"time": "2026-10-01T20:10:22.456111-04:00", "frame": 93, "detect_ms": 157.3, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 92}}
{"time": "2026-10-01T20:10:22.615327-04:00", "frame": 94, "detect_ms": 156.4, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 93}}
{"time": "2026-10-01T20:10:22.774092-04:00", "frame": 95, "detect_ms": 156.0, "target": null, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 94}}
{"time": "2026-10-01T20:10:22.933237-04:00", "frame": 96, "detect_ms": 156.5, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 95}}
{"time": "2026-10-01T20:10:23.092941-04:00", "frame": 97, "detect_ms": 157.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 96}}
{"time": "2026-10-01T20:10:23.252935-04:00", "frame": 98, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 97}}
{"time": "2026-10-01T20:10:23.412158-04:00", "frame": 99, "detect_ms": 156.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 98}}
{"time": "2026-10-01T20:10:23.571356-04:00", "frame": 100, "detect_ms": 156.3, "target": {"x": 80, "y": 80, "width": 81, "height": 160, "score": 0.42434873792808503}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 99}}
{"time": "2026-10-01T20:10:23.731371-04:00", "frame": 101, "detect_ms": 157.1, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 100}}
{"time": "2026-10-01T20:10:23.891659-04:00", "frame": 102, "detect_ms": 157.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 101}}
{"time": "2026-10-01T20:10:24.051610-04:00", "frame": 103, "detect_ms": 156.9, "target": {"x": 62, "y": 78, "width": 84, "height": 162, "score": 0.573510674526915}, "gaze": {"x": 322, "y": 661, "valid": true, "sequence": 102}}
{"time": "2026-10-01T20:10:24.211227-04:00", "frame": 104, "detect_ms": 156.7, "target": {"x": 54, "y": 80, "width": 81, "height": 160, "score": 0.7423454946838319}, "gaze": {"x": 294, "y": 665, "valid": true, "sequence": 103}}
{"time": "2026-10-01T20:10:24.370411-04:00", "frame": 105, "detect_ms": 156.3, "target": {"x": 38, "y": 75, "width": 82, "height": 164, "score": 1.1452426933683455}, "gaze": {"x": 244, "y": 652, "valid": true, "sequence": 104}}
{"time": "2026-10-01T20:10:24.529884-04:00", "frame": 106, "detect_ms": 156.7, "target": {"x": 27, "y": 70, "width": 86, "height": 170, "score": 0.36968476697802544}, "gaze": {"x": 216, "y": 644, "valid": true, "sequence": 105}}
{"time": "2026-10-01T20:10:24.689649-04:00", "frame": 107, "detect_ms": 156.8, "target": {"x": 27, "y": 70, "width": 86, "height": 170, "score": 0.5839128571096808}, "gaze": {"x": 216, "y": 644, "valid": true, "sequence": 106}}
{"time": "2026-10-01T20:10:24.849065-04:00", "frame": 108, "detect_ms": 156.7, "target": null, "gaze": {"x": 216, "y": 644, "valid": true, "sequence": 107}}
{"time": "2026-10-01T20:10:25.007756-04:00", "frame": 109, "detect_ms": 155.7, "target": {"x": 31, "y": 75, "width": 82, "height": 164, "score": 0.1410879447357729}, "gaze": {"x": 222, "y": 652, "valid": true, "sequence": 108}}
{"time": "2026-10-01T20:10:25.167020-04:00", "frame": 110, "detect_ms": 156.4, "target": {"x": 38, "y": 70, "width": 86, "height": 170, "score": 0.5170947599690408}, "gaze": {"x": 250, "y": 644, "valid": true, "sequence": 109}}
{"time": "2026-10-01T20:10:25.326540-04:00", "frame": 111, "detect_ms": 156.5, "target": {"x": 47, "y": 73, "width": 84, "height": 167, "score": 0.7141025791643187}, "gaze": {"x": 275, "y": 652, "valid": true, "sequence": 110}}
{"time": "2026-10-01T20:10:25.487278-04:00", "frame": 112, "detect_ms": 156.8, "target": {"x": 56, "y": 76, "width": 82, "height": 164, "score": 1.1083704809425399}, "gaze": {"x": 300, "y": 656, "valid": true, "sequence": 111}}
{"time": "2026-10-01T20:10:25.647261-04:00", "frame": 113, "detect_ms": 157.0, "target": {"x": 60, "y": 74, "width": 81, "height": 162, "score": 0.46646928368136287}, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 112}}
{"time": "2026-10-01T20:10:25.806110-04:00", "frame": 114, "detect_ms": 156.0, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 113}}
{"time": "2026-10-01T20:10:25.964978-04:00", "frame": 115, "detect_ms": 156.1, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 114}}
{"time": "2026-10-01T20:10:26.124168-04:00", "frame": 116, "detect_ms": 156.6, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 115}}
{"time": "2026-10-01T20:10:26.283493-04:00", "frame": 117, "detect_ms": 156.5, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 116}}
{"time": "2026-10-01T20:10:26.442262-04:00", "frame": 118, "detect_ms": 155.9, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 117}}
{"time": "2026-10-01T20:10:26.601266-04:00", "frame": 119, "detect_ms": 156.3, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 118}}
{"time": "2026-10-01T20:10:26.760713-04:00", "frame": 120, "detect_ms": 156.8, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 119}}
{"time": "2026-10-01T20:10:26.920771-04:00", "frame": 121, "detect_ms": 156.9, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 120}}
{"time": "2026-10-01T20:10:27.079916-04:00", "frame": 122, "detect_ms": 156.4, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 121}}
{"time": "2026-10-01T20:10:27.238691-04:00", "frame": 123, "detect_ms": 156.0, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 122}}
{"time": "2026-10-01T20:10:27.397545-04:00", "frame": 124, "detect_ms": 156.1, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 123}}
{"time": "2026-10-01T20:10:27.556885-04:00", "frame": 125, "detect_ms": 156.7, "target": null, "gaze": {"x": 313, "y": 644, "valid": true, "sequence": 124}}
{"time": "2026-10-01T20:10:27.716194-04:00", "frame": 126, "detect_ms": 156.5, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 125}}
{"time": "2026-10-01T20:10:27.875092-04:00", "frame": 127, "detect_ms": 156.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 126}}
{"time": "2026-10-01T20:10:28.034317-04:00", "frame": 128, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 127}}
{"time": "2026-10-01T20:10:28.193329-04:00", "frame": 129, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 128}}
{"time": "2026-10-01T20:10:28.353246-04:00", "frame": 130, "detect_ms": 156.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 129}}
{"time": "2026-10-01T20:10:28.512877-04:00", "frame": 131, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 130}}
{"time": "2026-10-01T20:10:28.671850-04:00", "frame": 132, "detect_ms": 156.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 131}}
{"time": "2026-10-01T20:10:28.831284-04:00", "frame": 133, "detect_ms": 156.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 132}}
{"time": "2026-10-01T20:10:28.990658-04:00", "frame": 134, "detect_ms": 156.5, "target": {"x": 37, "y": 78, "width": 82, "height": 162, "score": 0.3567493313457817}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 133}}
{"time": "2026-10-01T20:10:29.150614-04:00", "frame": 135, "detect_ms": 156.7, "target": {"x": 32, "y": 83, "width": 76, "height": 151, "score": 0.5794487876701169}, "gaze": {"x": 216, "y": 661, "valid": true, "sequence": 134}}
{"time": "2026-10-01T20:10:29.309632-04:00", "frame": 136, "detect_ms": 156.1, "target": {"x": 17, "y": 82, "width": 82, "height": 158, "score": 0.9869850248796865}, "gaze": {"x": 178, "y": 669, "valid": true, "sequence": 135}}
{"time": "2026-10-01T20:10:29.468492-04:00", "frame": 137, "detect_ms": 156.0, "target": {"x": 10, "y": 78, "width": 82, "height": 162, "score": 0.8970985103223938}, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 136}}
{"time": "2026-10-01T20:10:29.627406-04:00", "frame": 138, "detect_ms": 156.2, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 137}}
{"time": "2026-10-01T20:10:29.786862-04:00", "frame": 139, "detect_ms": 156.5, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 138}}
{"time": "2026-10-01T20:10:29.946526-04:00", "frame": 140, "detect_ms": 156.8, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 139}}
{"time": "2026-10-01T20:10:30.105721-04:00", "frame": 141, "detect_ms": 156.0, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 140}}
{"time": "2026-10-01T20:10:30.264668-04:00", "frame": 142, "detect_ms": 156.3, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 141}}
{"time": "2026-10-01T20:10:30.424007-04:00", "frame": 143, "detect_ms": 156.6, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 142}}
{"time": "2026-10-01T20:10:30.584235-04:00", "frame": 144, "detect_ms": 156.6, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 143}}
{"time": "2026-10-01T20:10:30.743633-04:00", "frame": 145, "detect_ms": 156.4, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 144}}
{"time": "2026-10-01T20:10:30.902224-04:00", "frame": 146, "detect_ms": 155.8, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 145}}
{"time": "2026-10-01T20:10:31.061137-04:00", "frame": 147, "detect_ms": 156.2, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 146}}
{"time": "2026-10-01T20:10:31.220444-04:00", "frame": 148, "detect_ms": 156.7, "target": null, "gaze": {"x": 156, "y": 661, "valid": true, "sequence": 147}}
{"time": "2026-10-01T20:10:31.379823-04:00", "frame": 149, "detect_ms": 156.4, "target": {"x": 0, "y": 85, "width": 66, "height": 152, "score": 0.9152967035770416}, "gaze": {"x": 100, "y": 669, "valid": true, "sequence": 148}}
{"time": "2026-10-01T20:10:31.539240-04:00", "frame": 150, "detect_ms": 156.3, "target": {"x": 0, "y": 86, "width": 68, "height": 148, "score": 0.9081206690752879}, "gaze": {"x": 103, "y": 665, "valid": true, "sequence": 149}}
{"time": "2026-10-01T20:10:31.699140-04:00", "frame": 151, "detect_ms": 157.0, "target": null, "gaze": {"x": 103, "y": 665, "valid": true, "sequence": 150}}
{"time": "2026-10-01T20:10:31.858607-04:00", "frame": 152, "detect_ms": 156.7, "target": {"x": 7, "y": 84, "width": 81, "height": 156, "score": 1.02721988549456}, "gaze": {"x": 147, "y": 673, "valid": true, "sequence": 151}}
{"time": "2026-10-01T20:10:32.018863-04:00", "frame": 153, "detect_ms": 157.2, "target": {"x": 15, "y": 85, "width": 80, "height": 155, "score": 0.8233131768647581}, "gaze": {"x": 169, "y": 677, "valid": true, "sequence": 152}}
{"time": "2026-10-01T20:10:32.178563-04:00", "frame": 154, "detect_ms": 156.8, "target": {"x": 28, "y": 85, "width": 76, "height": 152, "score": 1.2280552258598618}, "gaze": {"x": 203, "y": 669, "valid": true, "sequence": 153}}
{"time": "2026-10-01T20:10:32.337704-04:00", "frame": 155, "detect_ms": 156.2, "target": {"x": 37, "y": 83, "width": 79, "height": 157, "score": 1.2598841892322525}, "gaze": {"x": 238, "y": 673, "valid": true, "sequence": 154}}
{"time": "2026-10-01T20:10:32.497119-04:00", "frame": 156, "detect_ms": 156.5, "target": {"x": 48, "y": 86, "width": 76, "height": 151, "score": 0.3985349233262241}, "gaze": {"x": 266, "y": 673, "valid": true, "sequence": 155}}
{"time": "2026-10-01T20:10:32.656703-04:00", "frame": 157, "detect_ms": 156.8, "target": {"x": 59, "y": 88, "width": 72, "height": 144, "score": 1.0283592687919736}, "gaze": {"x": 294, "y": 665, "valid": true, "sequence": 156}}
@@ -0,0 +1,158 @@
{"time": "2026-10-01T20:13:51.419708-04:00", "frame": 1, "detect_ms": 161.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 0}}
{"time": "2026-10-01T20:13:51.578978-04:00", "frame": 2, "detect_ms": 155.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 1}}
{"time": "2026-10-01T20:13:51.737659-04:00", "frame": 3, "detect_ms": 155.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 2}}
{"time": "2026-10-01T20:13:51.896076-04:00", "frame": 4, "detect_ms": 155.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 3}}
{"time": "2026-10-01T20:13:52.057477-04:00", "frame": 5, "detect_ms": 157.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 4}}
{"time": "2026-10-01T20:13:52.215840-04:00", "frame": 6, "detect_ms": 155.6, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 5}}
{"time": "2026-10-01T20:13:52.374520-04:00", "frame": 7, "detect_ms": 155.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 6}}
{"time": "2026-10-01T20:13:52.532478-04:00", "frame": 8, "detect_ms": 155.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 7}}
{"time": "2026-10-01T20:13:52.691585-04:00", "frame": 9, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 8}}
{"time": "2026-10-01T20:13:52.850304-04:00", "frame": 10, "detect_ms": 155.9, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 9}}
{"time": "2026-10-01T20:13:53.008882-04:00", "frame": 11, "detect_ms": 155.0, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 10}}
{"time": "2026-10-01T20:13:53.167533-04:00", "frame": 12, "detect_ms": 155.8, "target": {"x": 120, "y": 49, "width": 88, "height": 176, "score": 0.8292022676905617}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 11}}
{"time": "2026-10-01T20:13:53.326053-04:00", "frame": 13, "detect_ms": 155.8, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 12}}
{"time": "2026-10-01T20:13:53.485139-04:00", "frame": 14, "detect_ms": 156.3, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 13}}
{"time": "2026-10-01T20:13:53.643324-04:00", "frame": 15, "detect_ms": 155.2, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 14}}
{"time": "2026-10-01T20:13:53.801722-04:00", "frame": 16, "detect_ms": 155.7, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 15}}
{"time": "2026-10-01T20:13:53.960775-04:00", "frame": 17, "detect_ms": 156.4, "target": null, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 16}}
{"time": "2026-10-01T20:13:54.119795-04:00", "frame": 18, "detect_ms": 155.8, "target": {"x": 82, "y": 50, "width": 88, "height": 176, "score": 0.3830644800327718}, "gaze": {"x": 500, "y": 500, "valid": false, "sequence": 17}}
{"time": "2026-10-01T20:13:54.278249-04:00", "frame": 19, "detect_ms": 155.5, "target": {"x": 82, "y": 50, "width": 88, "height": 176, "score": 0.41783385118469596}, "gaze": {"x": 391, "y": 573, "valid": true, "sequence": 18}}
{"time": "2026-10-01T20:13:54.436481-04:00", "frame": 20, "detect_ms": 155.4, "target": {"x": 82, "y": 50, "width": 88, "height": 176, "score": 0.1677097782958299}, "gaze": {"x": 391, "y": 573, "valid": true, "sequence": 19}}
{"time": "2026-10-01T20:13:54.595145-04:00", "frame": 21, "detect_ms": 155.7, "target": {"x": 75, "y": 58, "width": 83, "height": 167, "score": 0.549407378421165}, "gaze": {"x": 363, "y": 589, "valid": true, "sequence": 20}}
{"time": "2026-10-01T20:13:54.755420-04:00", "frame": 22, "detect_ms": 156.6, "target": {"x": 70, "y": 48, "width": 86, "height": 172, "score": 0.8829302424564958}, "gaze": {"x": 351, "y": 556, "valid": true, "sequence": 21}}
{"time": "2026-10-01T20:13:54.914268-04:00", "frame": 23, "detect_ms": 155.9, "target": {"x": 66, "y": 55, "width": 81, "height": 162, "score": 1.0424092205939814}, "gaze": {"x": 332, "y": 564, "valid": true, "sequence": 22}}
{"time": "2026-10-01T20:13:55.072444-04:00", "frame": 24, "detect_ms": 155.4, "target": {"x": 56, "y": 51, "width": 85, "height": 169, "score": 0.8664425248280168}, "gaze": {"x": 307, "y": 564, "valid": true, "sequence": 23}}
{"time": "2026-10-01T20:13:55.230946-04:00", "frame": 25, "detect_ms": 155.8, "target": {"x": 46, "y": 49, "width": 87, "height": 175, "score": 1.2748883119784296}, "gaze": {"x": 278, "y": 569, "valid": true, "sequence": 24}}
{"time": "2026-10-01T20:13:55.389607-04:00", "frame": 26, "detect_ms": 155.9, "target": {"x": 41, "y": 54, "width": 86, "height": 172, "score": 1.0381227741017938}, "gaze": {"x": 260, "y": 581, "valid": true, "sequence": 25}}
{"time": "2026-10-01T20:13:55.548335-04:00", "frame": 27, "detect_ms": 156.0, "target": null, "gaze": {"x": 260, "y": 581, "valid": true, "sequence": 26}}
{"time": "2026-10-01T20:13:55.706243-04:00", "frame": 28, "detect_ms": 155.1, "target": null, "gaze": {"x": 260, "y": 581, "valid": true, "sequence": 27}}
{"time": "2026-10-01T20:13:55.864511-04:00", "frame": 29, "detect_ms": 155.5, "target": {"x": 32, "y": 88, "width": 70, "height": 139, "score": 0.9252765695564449}, "gaze": {"x": 206, "y": 656, "valid": true, "sequence": 28}}
{"time": "2026-10-01T20:13:56.023488-04:00", "frame": 30, "detect_ms": 156.2, "target": {"x": 26, "y": 90, "width": 69, "height": 138, "score": 0.8658018924179487}, "gaze": {"x": 188, "y": 661, "valid": true, "sequence": 29}}
{"time": "2026-10-01T20:13:56.182899-04:00", "frame": 31, "detect_ms": 156.3, "target": {"x": 21, "y": 83, "width": 74, "height": 148, "score": 0.6627820816356689}, "gaze": {"x": 178, "y": 652, "valid": true, "sequence": 30}}
{"time": "2026-10-01T20:13:56.341456-04:00", "frame": 32, "detect_ms": 155.5, "target": {"x": 16, "y": 66, "width": 81, "height": 161, "score": 0.9211454490432516}, "gaze": {"x": 175, "y": 610, "valid": true, "sequence": 31}}
{"time": "2026-10-01T20:13:56.499667-04:00", "frame": 33, "detect_ms": 155.4, "target": {"x": 14, "y": 65, "width": 84, "height": 167, "score": 0.9578765626065433}, "gaze": {"x": 172, "y": 619, "valid": true, "sequence": 32}}
{"time": "2026-10-01T20:13:56.658467-04:00", "frame": 34, "detect_ms": 156.0, "target": {"x": 11, "y": 55, "width": 88, "height": 176, "score": 0.44668142520822585}, "gaze": {"x": 169, "y": 594, "valid": true, "sequence": 33}}
{"time": "2026-10-01T20:13:56.817586-04:00", "frame": 35, "detect_ms": 156.2, "target": null, "gaze": {"x": 169, "y": 594, "valid": true, "sequence": 34}}
{"time": "2026-10-01T20:13:56.975915-04:00", "frame": 36, "detect_ms": 155.6, "target": null, "gaze": {"x": 169, "y": 594, "valid": true, "sequence": 35}}
{"time": "2026-10-01T20:13:57.134429-04:00", "frame": 37, "detect_ms": 155.6, "target": {"x": 34, "y": 60, "width": 80, "height": 160, "score": 0.10272665740922093}, "gaze": {"x": 228, "y": 581, "valid": true, "sequence": 36}}
{"time": "2026-10-01T20:13:57.293260-04:00", "frame": 38, "detect_ms": 156.0, "target": {"x": 36, "y": 58, "width": 82, "height": 164, "score": 0.2744307345710695}, "gaze": {"x": 238, "y": 581, "valid": true, "sequence": 37}}
{"time": "2026-10-01T20:13:57.452673-04:00", "frame": 39, "detect_ms": 156.1, "target": {"x": 46, "y": 58, "width": 78, "height": 156, "score": 0.7577221662504598}, "gaze": {"x": 263, "y": 564, "valid": true, "sequence": 38}}
{"time": "2026-10-01T20:13:57.611358-04:00", "frame": 40, "detect_ms": 155.8, "target": {"x": 45, "y": 51, "width": 88, "height": 175, "score": 1.611471845768392}, "gaze": {"x": 275, "y": 577, "valid": true, "sequence": 39}}
{"time": "2026-10-01T20:13:57.769801-04:00", "frame": 41, "detect_ms": 155.3, "target": {"x": 50, "y": 55, "width": 83, "height": 166, "score": 1.2659578495658934}, "gaze": {"x": 285, "y": 573, "valid": true, "sequence": 40}}
{"time": "2026-10-01T20:13:57.928215-04:00", "frame": 42, "detect_ms": 155.7, "target": {"x": 59, "y": 52, "width": 83, "height": 166, "score": 0.8591726269805804}, "gaze": {"x": 313, "y": 560, "valid": true, "sequence": 41}}
{"time": "2026-10-01T20:13:58.087520-04:00", "frame": 43, "detect_ms": 156.2, "target": {"x": 69, "y": 53, "width": 79, "height": 158, "score": 1.3220722881378606}, "gaze": {"x": 338, "y": 548, "valid": true, "sequence": 42}}
{"time": "2026-10-01T20:13:58.246659-04:00", "frame": 44, "detect_ms": 155.9, "target": {"x": 73, "y": 49, "width": 82, "height": 164, "score": 0.8883316224091686}, "gaze": {"x": 354, "y": 543, "valid": true, "sequence": 43}}
{"time": "2026-10-01T20:13:58.404582-04:00", "frame": 45, "detect_ms": 155.1, "target": {"x": 80, "y": 48, "width": 86, "height": 172, "score": 0.31732277560513467}, "gaze": {"x": 382, "y": 556, "valid": true, "sequence": 44}}
{"time": "2026-10-01T20:13:58.563176-04:00", "frame": 46, "detect_ms": 155.8, "target": {"x": 95, "y": 55, "width": 74, "height": 148, "score": 0.6784135405905545}, "gaze": {"x": 410, "y": 535, "valid": true, "sequence": 45}}
{"time": "2026-10-01T20:13:58.722411-04:00", "frame": 47, "detect_ms": 156.5, "target": {"x": 106, "y": 56, "width": 71, "height": 141, "score": 0.5507696587592363}, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 46}}
{"time": "2026-10-01T20:13:58.881085-04:00", "frame": 48, "detect_ms": 155.9, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 47}}
{"time": "2026-10-01T20:13:59.039206-04:00", "frame": 49, "detect_ms": 155.3, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 48}}
{"time": "2026-10-01T20:13:59.197351-04:00", "frame": 50, "detect_ms": 155.4, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 49}}
{"time": "2026-10-01T20:13:59.356485-04:00", "frame": 51, "detect_ms": 156.4, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 50}}
{"time": "2026-10-01T20:13:59.515177-04:00", "frame": 52, "detect_ms": 155.9, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 51}}
{"time": "2026-10-01T20:13:59.673402-04:00", "frame": 53, "detect_ms": 155.2, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 52}}
{"time": "2026-10-01T20:13:59.831613-04:00", "frame": 54, "detect_ms": 155.5, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 53}}
{"time": "2026-10-01T20:13:59.991001-04:00", "frame": 55, "detect_ms": 156.7, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 54}}
{"time": "2026-10-01T20:14:00.152101-04:00", "frame": 56, "detect_ms": 157.7, "target": null, "gaze": {"x": 442, "y": 527, "valid": true, "sequence": 55}}
{"time": "2026-10-01T20:14:00.310845-04:00", "frame": 57, "detect_ms": 155.8, "target": {"x": 153, "y": 56, "width": 82, "height": 164, "score": 0.4511571351904422}, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 56}}
{"time": "2026-10-01T20:14:00.468715-04:00", "frame": 58, "detect_ms": 155.1, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 57}}
{"time": "2026-10-01T20:14:00.627521-04:00", "frame": 59, "detect_ms": 156.1, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 58}}
{"time": "2026-10-01T20:14:00.786653-04:00", "frame": 60, "detect_ms": 156.1, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 59}}
{"time": "2026-10-01T20:14:00.945293-04:00", "frame": 61, "detect_ms": 155.7, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 60}}
{"time": "2026-10-01T20:14:01.103491-04:00", "frame": 62, "detect_ms": 155.5, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 61}}
{"time": "2026-10-01T20:14:01.261558-04:00", "frame": 63, "detect_ms": 155.4, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 62}}
{"time": "2026-10-01T20:14:01.420069-04:00", "frame": 64, "detect_ms": 156.0, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 63}}
{"time": "2026-10-01T20:14:01.579501-04:00", "frame": 65, "detect_ms": 156.6, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 64}}
{"time": "2026-10-01T20:14:01.737531-04:00", "frame": 66, "detect_ms": 155.2, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 65}}
{"time": "2026-10-01T20:14:01.895707-04:00", "frame": 67, "detect_ms": 155.5, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 66}}
{"time": "2026-10-01T20:14:02.054906-04:00", "frame": 68, "detect_ms": 156.6, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 67}}
{"time": "2026-10-01T20:14:02.213489-04:00", "frame": 69, "detect_ms": 155.8, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 68}}
{"time": "2026-10-01T20:14:02.371417-04:00", "frame": 70, "detect_ms": 155.1, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 69}}
{"time": "2026-10-01T20:14:02.529908-04:00", "frame": 71, "detect_ms": 155.5, "target": null, "gaze": {"x": 605, "y": 573, "valid": true, "sequence": 70}}
{"time": "2026-10-01T20:14:02.688724-04:00", "frame": 72, "detect_ms": 156.1, "target": {"x": 69, "y": 49, "width": 82, "height": 165, "score": 1.6266744252061471}, "gaze": {"x": 341, "y": 548, "valid": true, "sequence": 71}}
{"time": "2026-10-01T20:14:02.847792-04:00", "frame": 73, "detect_ms": 156.1, "target": {"x": 63, "y": 50, "width": 82, "height": 163, "score": 0.5804882571101189}, "gaze": {"x": 322, "y": 548, "valid": true, "sequence": 72}}
{"time": "2026-10-01T20:14:03.006112-04:00", "frame": 74, "detect_ms": 155.3, "target": {"x": 57, "y": 47, "width": 84, "height": 168, "score": 0.122380290646106}, "gaze": {"x": 307, "y": 543, "valid": true, "sequence": 73}}
{"time": "2026-10-01T20:14:03.164702-04:00", "frame": 75, "detect_ms": 155.8, "target": {"x": 44, "y": 44, "width": 88, "height": 177, "score": 1.3082398707047105}, "gaze": {"x": 272, "y": 552, "valid": true, "sequence": 74}}
{"time": "2026-10-01T20:14:03.323229-04:00", "frame": 76, "detect_ms": 155.9, "target": null, "gaze": {"x": 272, "y": 552, "valid": true, "sequence": 75}}
{"time": "2026-10-01T20:14:03.482771-04:00", "frame": 77, "detect_ms": 156.2, "target": {"x": 26, "y": 48, "width": 89, "height": 177, "score": 0.1996438300702721}, "gaze": {"x": 219, "y": 569, "valid": true, "sequence": 76}}
{"time": "2026-10-01T20:14:03.641341-04:00", "frame": 78, "detect_ms": 155.7, "target": {"x": 29, "y": 58, "width": 79, "height": 158, "score": 0.5008291450794786}, "gaze": {"x": 213, "y": 569, "valid": true, "sequence": 77}}
{"time": "2026-10-01T20:14:03.800363-04:00", "frame": 79, "detect_ms": 156.2, "target": {"x": 22, "y": 55, "width": 88, "height": 176, "score": 0.13915335410274565}, "gaze": {"x": 203, "y": 594, "valid": true, "sequence": 78}}
{"time": "2026-10-01T20:14:03.959703-04:00", "frame": 80, "detect_ms": 156.5, "target": {"x": 19, "y": 45, "width": 90, "height": 181, "score": 0.14158180495724082}, "gaze": {"x": 197, "y": 564, "valid": true, "sequence": 79}}
{"time": "2026-10-01T20:14:04.119223-04:00", "frame": 81, "detect_ms": 156.3, "target": {"x": 22, "y": 59, "width": 85, "height": 170, "score": 0.6982051872182637}, "gaze": {"x": 200, "y": 598, "valid": true, "sequence": 80}}
{"time": "2026-10-01T20:14:04.277992-04:00", "frame": 82, "detect_ms": 155.9, "target": {"x": 29, "y": 68, "width": 78, "height": 156, "score": 1.1853144839406013}, "gaze": {"x": 210, "y": 606, "valid": true, "sequence": 81}}
{"time": "2026-10-01T20:14:04.436393-04:00", "frame": 83, "detect_ms": 155.6, "target": {"x": 34, "y": 67, "width": 78, "height": 156, "score": 1.0237712251255289}, "gaze": {"x": 225, "y": 602, "valid": true, "sequence": 82}}
{"time": "2026-10-01T20:14:04.594734-04:00", "frame": 84, "detect_ms": 155.5, "target": {"x": 38, "y": 59, "width": 82, "height": 163, "score": 1.1950411116704345}, "gaze": {"x": 244, "y": 585, "valid": true, "sequence": 83}}
{"time": "2026-10-01T20:14:04.754213-04:00", "frame": 85, "detect_ms": 156.7, "target": {"x": 40, "y": 52, "width": 87, "height": 174, "score": 1.1142327887937427}, "gaze": {"x": 260, "y": 577, "valid": true, "sequence": 84}}
{"time": "2026-10-01T20:14:04.913340-04:00", "frame": 86, "detect_ms": 156.2, "target": {"x": 44, "y": 52, "width": 84, "height": 167, "score": 0.8280194564722478}, "gaze": {"x": 266, "y": 564, "valid": true, "sequence": 85}}
{"time": "2026-10-01T20:14:05.071946-04:00", "frame": 87, "detect_ms": 155.6, "target": {"x": 47, "y": 50, "width": 87, "height": 173, "score": 0.9995137741789222}, "gaze": {"x": 282, "y": 569, "valid": true, "sequence": 86}}
{"time": "2026-10-01T20:14:05.230872-04:00", "frame": 88, "detect_ms": 155.8, "target": {"x": 58, "y": 54, "width": 80, "height": 159, "score": 1.136348217842169}, "gaze": {"x": 304, "y": 556, "valid": true, "sequence": 87}}
{"time": "2026-10-01T20:14:05.389868-04:00", "frame": 89, "detect_ms": 156.1, "target": {"x": 65, "y": 50, "width": 80, "height": 160, "score": 1.0453037268016487}, "gaze": {"x": 326, "y": 539, "valid": true, "sequence": 88}}
{"time": "2026-10-01T20:14:05.549049-04:00", "frame": 90, "detect_ms": 156.1, "target": {"x": 71, "y": 50, "width": 78, "height": 156, "score": 0.4361996728403028}, "gaze": {"x": 341, "y": 531, "valid": true, "sequence": 89}}
{"time": "2026-10-01T20:14:05.707909-04:00", "frame": 91, "detect_ms": 155.8, "target": {"x": 76, "y": 46, "width": 82, "height": 164, "score": 0.5362331364303827}, "gaze": {"x": 363, "y": 531, "valid": true, "sequence": 90}}
{"time": "2026-10-01T20:14:05.866042-04:00", "frame": 92, "detect_ms": 155.3, "target": {"x": 83, "y": 48, "width": 83, "height": 166, "score": 0.4892149588558823}, "gaze": {"x": 388, "y": 543, "valid": true, "sequence": 91}}
{"time": "2026-10-01T20:14:06.025393-04:00", "frame": 93, "detect_ms": 156.6, "target": {"x": 96, "y": 55, "width": 74, "height": 146, "score": 1.1273685505148023}, "gaze": {"x": 413, "y": 531, "valid": true, "sequence": 92}}
{"time": "2026-10-01T20:14:06.184465-04:00", "frame": 94, "detect_ms": 156.1, "target": {"x": 93, "y": 48, "width": 83, "height": 166, "score": 0.9826249945908785}, "gaze": {"x": 420, "y": 543, "valid": true, "sequence": 93}}
{"time": "2026-10-01T20:14:06.343344-04:00", "frame": 95, "detect_ms": 156.0, "target": {"x": 99, "y": 48, "width": 89, "height": 177, "score": 0.701170151703991}, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 94}}
{"time": "2026-10-01T20:14:06.501307-04:00", "frame": 96, "detect_ms": 155.2, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 95}}
{"time": "2026-10-01T20:14:06.660080-04:00", "frame": 97, "detect_ms": 156.2, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 96}}
{"time": "2026-10-01T20:14:06.819305-04:00", "frame": 98, "detect_ms": 156.6, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 97}}
{"time": "2026-10-01T20:14:06.978038-04:00", "frame": 99, "detect_ms": 155.9, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 98}}
{"time": "2026-10-01T20:14:07.136234-04:00", "frame": 100, "detect_ms": 155.4, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 99}}
{"time": "2026-10-01T20:14:07.294797-04:00", "frame": 101, "detect_ms": 155.6, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 100}}
{"time": "2026-10-01T20:14:07.453525-04:00", "frame": 102, "detect_ms": 156.1, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 101}}
{"time": "2026-10-01T20:14:07.613072-04:00", "frame": 103, "detect_ms": 156.1, "target": null, "gaze": {"x": 448, "y": 569, "valid": true, "sequence": 102}}
{"time": "2026-10-01T20:14:07.771446-04:00", "frame": 104, "detect_ms": 155.5, "target": {"x": 119, "y": 58, "width": 81, "height": 161, "score": 1.284876958001405}, "gaze": {"x": 498, "y": 577, "valid": true, "sequence": 103}}
{"time": "2026-10-01T20:14:07.929580-04:00", "frame": 105, "detect_ms": 155.3, "target": {"x": 115, "y": 57, "width": 84, "height": 168, "score": 0.8896688905078918}, "gaze": {"x": 489, "y": 585, "valid": true, "sequence": 104}}
{"time": "2026-10-01T20:14:08.088695-04:00", "frame": 106, "detect_ms": 156.3, "target": {"x": 111, "y": 60, "width": 77, "height": 154, "score": 0.5198570486390963}, "gaze": {"x": 467, "y": 569, "valid": true, "sequence": 105}}
{"time": "2026-10-01T20:14:08.248411-04:00", "frame": 107, "detect_ms": 156.4, "target": {"x": 107, "y": 64, "width": 76, "height": 152, "score": 0.8044589868513867}, "gaze": {"x": 451, "y": 581, "valid": true, "sequence": 106}}
{"time": "2026-10-01T20:14:08.406949-04:00", "frame": 108, "detect_ms": 155.6, "target": {"x": 104, "y": 64, "width": 74, "height": 148, "score": 0.4137590081663802}, "gaze": {"x": 438, "y": 573, "valid": true, "sequence": 107}}
{"time": "2026-10-01T20:14:08.565299-04:00", "frame": 109, "detect_ms": 155.5, "target": {"x": 97, "y": 61, "width": 78, "height": 156, "score": 0.32509812735952437}, "gaze": {"x": 423, "y": 577, "valid": true, "sequence": 108}}
{"time": "2026-10-01T20:14:08.723636-04:00", "frame": 110, "detect_ms": 155.6, "target": {"x": 94, "y": 62, "width": 76, "height": 152, "score": 0.45426615700125694}, "gaze": {"x": 410, "y": 573, "valid": true, "sequence": 109}}
{"time": "2026-10-01T20:14:08.883439-04:00", "frame": 111, "detect_ms": 156.1, "target": {"x": 83, "y": 57, "width": 84, "height": 168, "score": 0.6687999197747558}, "gaze": {"x": 388, "y": 585, "valid": true, "sequence": 110}}
{"time": "2026-10-01T20:14:09.042317-04:00", "frame": 112, "detect_ms": 156.0, "target": {"x": 80, "y": 59, "width": 80, "height": 159, "score": 0.8042698130011559}, "gaze": {"x": 373, "y": 577, "valid": true, "sequence": 111}}
{"time": "2026-10-01T20:14:09.200482-04:00", "frame": 113, "detect_ms": 155.3, "target": {"x": 71, "y": 62, "width": 75, "height": 149, "score": 1.65706046205014}, "gaze": {"x": 338, "y": 569, "valid": true, "sequence": 112}}
{"time": "2026-10-01T20:14:09.359068-04:00", "frame": 114, "detect_ms": 155.5, "target": {"x": 61, "y": 61, "width": 76, "height": 153, "score": 2.0940143957850523}, "gaze": {"x": 307, "y": 573, "valid": true, "sequence": 113}}
{"time": "2026-10-01T20:14:09.517872-04:00", "frame": 115, "detect_ms": 156.1, "target": {"x": 51, "y": 60, "width": 78, "height": 156, "score": 1.1769480465445668}, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 114}}
{"time": "2026-10-01T20:14:09.676512-04:00", "frame": 116, "detect_ms": 155.9, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 115}}
{"time": "2026-10-01T20:14:09.836386-04:00", "frame": 117, "detect_ms": 157.0, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 116}}
{"time": "2026-10-01T20:14:09.994588-04:00", "frame": 118, "detect_ms": 155.5, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 117}}
{"time": "2026-10-01T20:14:10.153112-04:00", "frame": 119, "detect_ms": 155.8, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 118}}
{"time": "2026-10-01T20:14:10.312388-04:00", "frame": 120, "detect_ms": 156.1, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 119}}
{"time": "2026-10-01T20:14:10.471046-04:00", "frame": 121, "detect_ms": 155.7, "target": null, "gaze": {"x": 278, "y": 573, "valid": true, "sequence": 120}}
{"time": "2026-10-01T20:14:10.629247-04:00", "frame": 122, "detect_ms": 155.3, "target": {"x": 3, "y": 59, "width": 72, "height": 144, "score": 0.31605819531250745}, "gaze": {"x": 119, "y": 543, "valid": true, "sequence": 121}}
{"time": "2026-10-01T20:14:10.788708-04:00", "frame": 123, "detect_ms": 156.3, "target": {"x": 0, "y": 59, "width": 74, "height": 148, "score": 0.7620021866168827}, "gaze": {"x": 112, "y": 552, "valid": true, "sequence": 122}}
{"time": "2026-10-01T20:14:10.948093-04:00", "frame": 124, "detect_ms": 156.3, "target": {"x": 0, "y": 46, "width": 78, "height": 169, "score": 0.5825892710126936}, "gaze": {"x": 119, "y": 543, "valid": true, "sequence": 123}}
{"time": "2026-10-01T20:14:11.106959-04:00", "frame": 125, "detect_ms": 156.0, "target": {"x": 0, "y": 47, "width": 76, "height": 168, "score": 0.5374430458759889}, "gaze": {"x": 115, "y": 543, "valid": true, "sequence": 124}}
{"time": "2026-10-01T20:14:11.265144-04:00", "frame": 126, "detect_ms": 155.1, "target": {"x": 0, "y": 57, "width": 73, "height": 154, "score": 0.3712352456059307}, "gaze": {"x": 112, "y": 556, "valid": true, "sequence": 125}}
{"time": "2026-10-01T20:14:11.423718-04:00", "frame": 127, "detect_ms": 155.9, "target": {"x": 0, "y": 59, "width": 72, "height": 144, "score": 0.08541919966228306}, "gaze": {"x": 109, "y": 543, "valid": true, "sequence": 126}}
{"time": "2026-10-01T20:14:11.582675-04:00", "frame": 128, "detect_ms": 156.2, "target": {"x": 0, "y": 57, "width": 73, "height": 154, "score": 0.29203601996414363}, "gaze": {"x": 112, "y": 556, "valid": true, "sequence": 127}}
{"time": "2026-10-01T20:14:11.741996-04:00", "frame": 129, "detect_ms": 156.2, "target": {"x": 0, "y": 24, "width": 85, "height": 194, "score": 0.34693584288470447}, "gaze": {"x": 131, "y": 502, "valid": true, "sequence": 128}}
{"time": "2026-10-01T20:14:11.903620-04:00", "frame": 130, "detect_ms": 158.7, "target": {"x": 0, "y": 21, "width": 87, "height": 199, "score": 0.33722397964447737}, "gaze": {"x": 134, "y": 502, "valid": true, "sequence": 129}}
{"time": "2026-10-01T20:14:12.065156-04:00", "frame": 131, "detect_ms": 158.4, "target": {"x": 0, "y": 54, "width": 82, "height": 164, "score": 0.9866430150577798}, "gaze": {"x": 125, "y": 564, "valid": true, "sequence": 130}}
{"time": "2026-10-01T20:14:12.226479-04:00", "frame": 132, "detect_ms": 158.6, "target": {"x": 0, "y": 14, "width": 98, "height": 224, "score": 0.22611459461040795}, "gaze": {"x": 150, "y": 523, "valid": true, "sequence": 131}}
{"time": "2026-10-01T20:14:12.385588-04:00", "frame": 133, "detect_ms": 156.4, "target": {"x": 7, "y": 52, "width": 85, "height": 169, "score": 0.8269012661185116}, "gaze": {"x": 153, "y": 569, "valid": true, "sequence": 132}}
{"time": "2026-10-01T20:14:12.544525-04:00", "frame": 134, "detect_ms": 155.9, "target": null, "gaze": {"x": 153, "y": 569, "valid": true, "sequence": 133}}
{"time": "2026-10-01T20:14:12.702917-04:00", "frame": 135, "detect_ms": 155.6, "target": null, "gaze": {"x": 153, "y": 569, "valid": true, "sequence": 134}}
{"time": "2026-10-01T20:14:12.860996-04:00", "frame": 136, "detect_ms": 155.3, "target": null, "gaze": {"x": 153, "y": 569, "valid": true, "sequence": 135}}
{"time": "2026-10-01T20:14:13.019493-04:00", "frame": 137, "detect_ms": 155.9, "target": null, "gaze": {"x": 153, "y": 569, "valid": true, "sequence": 136}}
{"time": "2026-10-01T20:14:13.179302-04:00", "frame": 138, "detect_ms": 156.0, "target": {"x": 15, "y": 56, "width": 82, "height": 164, "score": 0.33663460763636976}, "gaze": {"x": 172, "y": 573, "valid": true, "sequence": 137}}
{"time": "2026-10-01T20:14:13.338567-04:00", "frame": 139, "detect_ms": 155.8, "target": {"x": 15, "y": 56, "width": 82, "height": 164, "score": 0.36107451654970646}, "gaze": {"x": 172, "y": 573, "valid": true, "sequence": 138}}
{"time": "2026-10-01T20:14:13.496525-04:00", "frame": 140, "detect_ms": 155.1, "target": {"x": 10, "y": 58, "width": 84, "height": 168, "score": 0.28044524416327477}, "gaze": {"x": 159, "y": 589, "valid": true, "sequence": 139}}
{"time": "2026-10-01T20:14:13.655476-04:00", "frame": 141, "detect_ms": 155.9, "target": {"x": 15, "y": 56, "width": 82, "height": 164, "score": 0.10148746886989102}, "gaze": {"x": 172, "y": 573, "valid": true, "sequence": 140}}
{"time": "2026-10-01T20:14:13.814239-04:00", "frame": 142, "detect_ms": 155.9, "target": null, "gaze": {"x": 172, "y": 573, "valid": true, "sequence": 141}}
{"time": "2026-10-01T20:14:13.973058-04:00", "frame": 143, "detect_ms": 156.1, "target": null, "gaze": {"x": 172, "y": 573, "valid": true, "sequence": 142}}
{"time": "2026-10-01T20:14:14.131426-04:00", "frame": 144, "detect_ms": 155.5, "target": {"x": 10, "y": 58, "width": 84, "height": 168, "score": 0.2630695402913261}, "gaze": {"x": 159, "y": 589, "valid": true, "sequence": 143}}
{"time": "2026-10-01T20:14:14.289718-04:00", "frame": 145, "detect_ms": 155.5, "target": {"x": 13, "y": 63, "width": 81, "height": 161, "score": 0.8461061392445117}, "gaze": {"x": 166, "y": 598, "valid": true, "sequence": 144}}
{"time": "2026-10-01T20:14:14.448641-04:00", "frame": 146, "detect_ms": 156.2, "target": {"x": 14, "y": 62, "width": 76, "height": 152, "score": 0.7176651358604431}, "gaze": {"x": 159, "y": 573, "valid": true, "sequence": 145}}
{"time": "2026-10-01T20:14:14.607475-04:00", "frame": 147, "detect_ms": 155.9, "target": {"x": 14, "y": 62, "width": 76, "height": 152, "score": 0.6424067683983594}, "gaze": {"x": 159, "y": 573, "valid": true, "sequence": 146}}
{"time": "2026-10-01T20:14:14.765950-04:00", "frame": 148, "detect_ms": 155.4, "target": {"x": 13, "y": 57, "width": 84, "height": 168, "score": 0.15905224857851863}, "gaze": {"x": 169, "y": 585, "valid": true, "sequence": 147}}
{"time": "2026-10-01T20:14:14.924393-04:00", "frame": 149, "detect_ms": 155.6, "target": {"x": 14, "y": 62, "width": 76, "height": 152, "score": 0.5673628260847181}, "gaze": {"x": 159, "y": 573, "valid": true, "sequence": 148}}
{"time": "2026-10-01T20:14:15.083311-04:00", "frame": 150, "detect_ms": 156.1, "target": {"x": 12, "y": 61, "width": 79, "height": 158, "score": 0.521738993935287}, "gaze": {"x": 159, "y": 581, "valid": true, "sequence": 149}}
{"time": "2026-10-01T20:14:15.242870-04:00", "frame": 151, "detect_ms": 156.0, "target": {"x": 14, "y": 62, "width": 76, "height": 152, "score": 0.3514394990634173}, "gaze": {"x": 159, "y": 573, "valid": true, "sequence": 150}}
{"time": "2026-10-01T20:14:15.401309-04:00", "frame": 152, "detect_ms": 155.5, "target": {"x": 16, "y": 62, "width": 76, "height": 152, "score": 0.3718896077480167}, "gaze": {"x": 166, "y": 573, "valid": true, "sequence": 151}}
{"time": "2026-10-01T20:14:15.559779-04:00", "frame": 153, "detect_ms": 155.7, "target": {"x": 17, "y": 64, "width": 77, "height": 154, "score": 0.8527774051763117}, "gaze": {"x": 172, "y": 585, "valid": true, "sequence": 152}}
{"time": "2026-10-01T20:14:15.718529-04:00", "frame": 154, "detect_ms": 155.9, "target": {"x": 22, "y": 64, "width": 74, "height": 147, "score": 0.5903336191549897}, "gaze": {"x": 181, "y": 573, "valid": true, "sequence": 153}}
{"time": "2026-10-01T20:14:15.877936-04:00", "frame": 155, "detect_ms": 156.0, "target": {"x": 29, "y": 67, "width": 69, "height": 137, "score": 0.6283593289554119}, "gaze": {"x": 197, "y": 564, "valid": true, "sequence": 154}}
{"time": "2026-10-01T20:14:16.036416-04:00", "frame": 156, "detect_ms": 155.6, "target": {"x": 29, "y": 64, "width": 71, "height": 142, "score": 0.25669746473431587}, "gaze": {"x": 200, "y": 560, "valid": true, "sequence": 155}}
{"time": "2026-10-01T20:14:16.194396-04:00", "frame": 157, "detect_ms": 155.2, "target": null, "gaze": {"x": 200, "y": 560, "valid": true, "sequence": 156}}
{"time": "2026-10-01T20:14:16.352898-04:00", "frame": 158, "detect_ms": 155.8, "target": null, "gaze": {"x": 200, "y": 560, "valid": true, "sequence": 157}}
+94
View File
@@ -0,0 +1,94 @@
{"time": "2026-10-01T20:02:03.432641-04:00", "frame": 1, "detect_ms": 178.2, "detections": []}
{"time": "2026-10-01T20:02:03.599254-04:00", "frame": 2, "detect_ms": 157.0, "detections": []}
{"time": "2026-10-01T20:02:03.761484-04:00", "frame": 3, "detect_ms": 159.5, "detections": []}
{"time": "2026-10-01T20:02:03.947533-04:00", "frame": 4, "detect_ms": 182.1, "detections": []}
{"time": "2026-10-01T20:02:04.106192-04:00", "frame": 5, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:02:04.264976-04:00", "frame": 6, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:02:04.424033-04:00", "frame": 7, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:04.583945-04:00", "frame": 8, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:04.743102-04:00", "frame": 9, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:04.901785-04:00", "frame": 10, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:05.061517-04:00", "frame": 11, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:05.220552-04:00", "frame": 12, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:05.380033-04:00", "frame": 13, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:05.539302-04:00", "frame": 14, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:05.698535-04:00", "frame": 15, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:05.857656-04:00", "frame": 16, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:06.018740-04:00", "frame": 17, "detect_ms": 157.5, "detections": []}
{"time": "2026-10-01T20:02:06.177740-04:00", "frame": 18, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:06.336258-04:00", "frame": 19, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:02:06.494998-04:00", "frame": 20, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:06.654301-04:00", "frame": 21, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:06.813744-04:00", "frame": 22, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:06.972747-04:00", "frame": 23, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:07.131703-04:00", "frame": 24, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:07.290649-04:00", "frame": 25, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:07.450714-04:00", "frame": 26, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:07.609738-04:00", "frame": 27, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:07.768808-04:00", "frame": 28, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:07.927559-04:00", "frame": 29, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:08.086923-04:00", "frame": 30, "detect_ms": 157.0, "detections": []}
{"time": "2026-10-01T20:02:08.246720-04:00", "frame": 31, "detect_ms": 157.0, "detections": []}
{"time": "2026-10-01T20:02:08.405344-04:00", "frame": 32, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:02:08.564204-04:00", "frame": 33, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:08.723195-04:00", "frame": 34, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:08.882551-04:00", "frame": 35, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:09.041775-04:00", "frame": 36, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:09.200346-04:00", "frame": 37, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:09.359110-04:00", "frame": 38, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:09.517991-04:00", "frame": 39, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:09.677072-04:00", "frame": 40, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:09.835881-04:00", "frame": 41, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:09.994445-04:00", "frame": 42, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:10.153785-04:00", "frame": 43, "detect_ms": 157.0, "detections": []}
{"time": "2026-10-01T20:02:10.313240-04:00", "frame": 44, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:10.472212-04:00", "frame": 45, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:10.630940-04:00", "frame": 46, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:10.790032-04:00", "frame": 47, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:10.949510-04:00", "frame": 48, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:11.108618-04:00", "frame": 49, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:11.267275-04:00", "frame": 50, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:11.426464-04:00", "frame": 51, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:11.585638-04:00", "frame": 52, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:11.745001-04:00", "frame": 53, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:11.904129-04:00", "frame": 54, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:12.063118-04:00", "frame": 55, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:12.221862-04:00", "frame": 56, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:12.381912-04:00", "frame": 57, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:12.540929-04:00", "frame": 58, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:12.699516-04:00", "frame": 59, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:02:12.857979-04:00", "frame": 60, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:02:13.017456-04:00", "frame": 61, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:13.176779-04:00", "frame": 62, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:13.335724-04:00", "frame": 63, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:13.494758-04:00", "frame": 64, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:13.653607-04:00", "frame": 65, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:13.812802-04:00", "frame": 66, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:13.971738-04:00", "frame": 67, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:14.130752-04:00", "frame": 68, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:02:14.289701-04:00", "frame": 69, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:14.449095-04:00", "frame": 70, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:14.608687-04:00", "frame": 71, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:14.767172-04:00", "frame": 72, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:02:14.925998-04:00", "frame": 73, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:15.085252-04:00", "frame": 74, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:15.245134-04:00", "frame": 75, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:15.403842-04:00", "frame": 76, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:02:15.562645-04:00", "frame": 77, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:02:15.722019-04:00", "frame": 78, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:15.881569-04:00", "frame": 79, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:16.040798-04:00", "frame": 80, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:16.199885-04:00", "frame": 81, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:16.358567-04:00", "frame": 82, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:02:16.518214-04:00", "frame": 83, "detect_ms": 157.2, "detections": []}
{"time": "2026-10-01T20:02:16.678073-04:00", "frame": 84, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:02:16.837088-04:00", "frame": 85, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:02:16.996405-04:00", "frame": 86, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:02:17.155789-04:00", "frame": 87, "detect_ms": 156.7, "detections": []}
{"time": "2026-10-01T20:02:17.314815-04:00", "frame": 88, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:17.474079-04:00", "frame": 89, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:17.632937-04:00", "frame": 90, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:02:17.792203-04:00", "frame": 91, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:02:17.952061-04:00", "frame": 92, "detect_ms": 157.4, "detections": []}
{"time": "2026-10-01T20:02:18.112498-04:00", "frame": 93, "detect_ms": 157.1, "detections": []}
{"time": "2026-10-01T20:02:18.271978-04:00", "frame": 94, "detect_ms": 156.8, "detections": []}
+126
View File
@@ -0,0 +1,126 @@
{"time": "2026-10-01T20:03:44.912049-04:00", "frame": 1, "detect_ms": 191.9, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.9743679778184742}]}
{"time": "2026-10-01T20:03:45.071640-04:00", "frame": 2, "detect_ms": 156.1, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.7492739096633159}]}
{"time": "2026-10-01T20:03:45.229928-04:00", "frame": 3, "detect_ms": 155.4, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.6991289030993357}]}
{"time": "2026-10-01T20:03:45.388718-04:00", "frame": 4, "detect_ms": 156.1, "detections": [{"x": 66, "y": 66, "width": 88, "height": 174, "score": 0.9594001601799391}]}
{"time": "2026-10-01T20:03:45.550741-04:00", "frame": 5, "detect_ms": 158.6, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.8903263994725421}]}
{"time": "2026-10-01T20:03:45.709269-04:00", "frame": 6, "detect_ms": 155.9, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 1.1929856976494193}]}
{"time": "2026-10-01T20:03:45.868911-04:00", "frame": 7, "detect_ms": 157.0, "detections": [{"x": 69, "y": 69, "width": 83, "height": 166, "score": 1.5159009881317616}]}
{"time": "2026-10-01T20:03:46.027164-04:00", "frame": 8, "detect_ms": 155.6, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.403458135901019}]}
{"time": "2026-10-01T20:03:46.185907-04:00", "frame": 9, "detect_ms": 156.2, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.6975642118777614}]}
{"time": "2026-10-01T20:03:46.344905-04:00", "frame": 10, "detect_ms": 156.3, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.7158208764158189}]}
{"time": "2026-10-01T20:03:46.504053-04:00", "frame": 11, "detect_ms": 155.9, "detections": [{"x": 67, "y": 70, "width": 86, "height": 170, "score": 0.4899646514095366}]}
{"time": "2026-10-01T20:03:46.662525-04:00", "frame": 12, "detect_ms": 155.8, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.6094202370150015}]}
{"time": "2026-10-01T20:03:46.821308-04:00", "frame": 13, "detect_ms": 156.3, "detections": [{"x": 68, "y": 68, "width": 86, "height": 172, "score": 0.6314901092555374}]}
{"time": "2026-10-01T20:03:46.980719-04:00", "frame": 14, "detect_ms": 156.4, "detections": [{"x": 67, "y": 70, "width": 86, "height": 170, "score": 0.3226635798346251}]}
{"time": "2026-10-01T20:03:47.139704-04:00", "frame": 15, "detect_ms": 156.3, "detections": [{"x": 66, "y": 74, "width": 82, "height": 164, "score": 0.2796173130045645}]}
{"time": "2026-10-01T20:03:47.298001-04:00", "frame": 16, "detect_ms": 155.7, "detections": [{"x": 58, "y": 70, "width": 86, "height": 170, "score": 0.30328784335870296}]}
{"time": "2026-10-01T20:03:47.456749-04:00", "frame": 17, "detect_ms": 156.3, "detections": [{"x": 58, "y": 70, "width": 86, "height": 170, "score": 1.2856615867931396}]}
{"time": "2026-10-01T20:03:47.615967-04:00", "frame": 18, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:03:47.774935-04:00", "frame": 19, "detect_ms": 156.3, "detections": [{"x": 48, "y": 70, "width": 86, "height": 170, "score": 1.2140943487756886}]}
{"time": "2026-10-01T20:03:47.933698-04:00", "frame": 20, "detect_ms": 156.1, "detections": [{"x": 40, "y": 68, "width": 89, "height": 172, "score": 0.14341444615274668}]}
{"time": "2026-10-01T20:03:48.092772-04:00", "frame": 21, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:03:48.251992-04:00", "frame": 22, "detect_ms": 156.7, "detections": [{"x": 35, "y": 74, "width": 85, "height": 166, "score": 0.5836917283013463}]}
{"time": "2026-10-01T20:03:48.411000-04:00", "frame": 23, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:03:48.569262-04:00", "frame": 24, "detect_ms": 155.5, "detections": [{"x": 36, "y": 75, "width": 86, "height": 165, "score": 0.38069694978185}]}
{"time": "2026-10-01T20:03:48.727701-04:00", "frame": 25, "detect_ms": 155.9, "detections": [{"x": 39, "y": 74, "width": 85, "height": 166, "score": 0.5316205252893269}]}
{"time": "2026-10-01T20:03:48.886508-04:00", "frame": 26, "detect_ms": 156.2, "detections": [{"x": 36, "y": 76, "width": 82, "height": 164, "score": 0.7941501098684967}]}
{"time": "2026-10-01T20:03:49.046300-04:00", "frame": 27, "detect_ms": 156.4, "detections": [{"x": 37, "y": 77, "width": 82, "height": 163, "score": 0.6563903992064297}]}
{"time": "2026-10-01T20:03:49.204634-04:00", "frame": 28, "detect_ms": 155.8, "detections": [{"x": 36, "y": 76, "width": 82, "height": 164, "score": 0.465165585745126}]}
{"time": "2026-10-01T20:03:49.363177-04:00", "frame": 29, "detect_ms": 156.0, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.38275245495606214}]}
{"time": "2026-10-01T20:03:49.521591-04:00", "frame": 30, "detect_ms": 155.9, "detections": [{"x": 36, "y": 76, "width": 84, "height": 164, "score": 0.8381900025415234}]}
{"time": "2026-10-01T20:03:49.681240-04:00", "frame": 31, "detect_ms": 156.4, "detections": [{"x": 36, "y": 75, "width": 86, "height": 165, "score": 0.8963113487698138}]}
{"time": "2026-10-01T20:03:49.839895-04:00", "frame": 32, "detect_ms": 156.1, "detections": [{"x": 39, "y": 71, "width": 86, "height": 169, "score": 0.6073798636207357}]}
{"time": "2026-10-01T20:03:49.998295-04:00", "frame": 33, "detect_ms": 155.8, "detections": [{"x": 46, "y": 71, "width": 84, "height": 169, "score": 1.7071477524004877}]}
{"time": "2026-10-01T20:03:50.156737-04:00", "frame": 34, "detect_ms": 156.0, "detections": [{"x": 63, "y": 79, "width": 78, "height": 156, "score": 1.6943111746804789}]}
{"time": "2026-10-01T20:03:50.315526-04:00", "frame": 35, "detect_ms": 156.3, "detections": [{"x": 71, "y": 77, "width": 81, "height": 162, "score": 1.1332876870874316}]}
{"time": "2026-10-01T20:03:50.474340-04:00", "frame": 36, "detect_ms": 156.1, "detections": [{"x": 78, "y": 71, "width": 84, "height": 168, "score": 0.3065724668558687}]}
{"time": "2026-10-01T20:03:50.632311-04:00", "frame": 37, "detect_ms": 155.4, "detections": []}
{"time": "2026-10-01T20:03:50.790790-04:00", "frame": 38, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:03:50.949305-04:00", "frame": 39, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:03:51.107808-04:00", "frame": 40, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:03:51.266096-04:00", "frame": 41, "detect_ms": 155.3, "detections": []}
{"time": "2026-10-01T20:03:51.424775-04:00", "frame": 42, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:03:51.583553-04:00", "frame": 43, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:03:51.742337-04:00", "frame": 44, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:03:51.900450-04:00", "frame": 45, "detect_ms": 155.4, "detections": []}
{"time": "2026-10-01T20:03:52.059054-04:00", "frame": 46, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:03:52.217557-04:00", "frame": 47, "detect_ms": 156.0, "detections": [{"x": 75, "y": 71, "width": 86, "height": 169, "score": 0.2949066136498004}]}
{"time": "2026-10-01T20:03:52.376587-04:00", "frame": 48, "detect_ms": 156.3, "detections": [{"x": 73, "y": 73, "width": 84, "height": 167, "score": 0.7805521166883409}]}
{"time": "2026-10-01T20:03:52.535076-04:00", "frame": 49, "detect_ms": 155.8, "detections": [{"x": 62, "y": 76, "width": 81, "height": 163, "score": 1.7535739468876272}]}
{"time": "2026-10-01T20:03:52.693744-04:00", "frame": 50, "detect_ms": 156.1, "detections": [{"x": 52, "y": 76, "width": 82, "height": 164, "score": 1.0097073897486553}]}
{"time": "2026-10-01T20:03:52.853080-04:00", "frame": 51, "detect_ms": 156.7, "detections": [{"x": 49, "y": 79, "width": 81, "height": 161, "score": 0.5663281250162981}]}
{"time": "2026-10-01T20:03:53.012183-04:00", "frame": 52, "detect_ms": 156.4, "detections": [{"x": 47, "y": 84, "width": 79, "height": 156, "score": 0.512150420807302}]}
{"time": "2026-10-01T20:03:53.170983-04:00", "frame": 53, "detect_ms": 156.1, "detections": [{"x": 45, "y": 84, "width": 80, "height": 156, "score": 0.6228424014989287}]}
{"time": "2026-10-01T20:03:53.329041-04:00", "frame": 54, "detect_ms": 155.4, "detections": [{"x": 44, "y": 88, "width": 78, "height": 152, "score": 0.28101144067477435}]}
{"time": "2026-10-01T20:03:53.487948-04:00", "frame": 55, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:03:53.646375-04:00", "frame": 56, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:03:53.805296-04:00", "frame": 57, "detect_ms": 156.3, "detections": [{"x": 40, "y": 85, "width": 80, "height": 155, "score": 0.23051152605330572}]}
{"time": "2026-10-01T20:03:53.963884-04:00", "frame": 58, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:03:54.122085-04:00", "frame": 59, "detect_ms": 155.6, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.2297352917958051}]}
{"time": "2026-10-01T20:03:54.281174-04:00", "frame": 60, "detect_ms": 156.3, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.2891608632635325}]}
{"time": "2026-10-01T20:03:54.440773-04:00", "frame": 61, "detect_ms": 156.8, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.12948257778771222}]}
{"time": "2026-10-01T20:03:54.599089-04:00", "frame": 62, "detect_ms": 155.5, "detections": [{"x": 40, "y": 85, "width": 80, "height": 155, "score": 0.18806367996148765}]}
{"time": "2026-10-01T20:03:54.757828-04:00", "frame": 63, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:03:54.916659-04:00", "frame": 64, "detect_ms": 156.4, "detections": [{"x": 38, "y": 81, "width": 81, "height": 159, "score": 0.5722653694683686}]}
{"time": "2026-10-01T20:03:55.075955-04:00", "frame": 65, "detect_ms": 156.2, "detections": [{"x": 37, "y": 78, "width": 83, "height": 162, "score": 0.3136994164669886}]}
{"time": "2026-10-01T20:03:55.234127-04:00", "frame": 66, "detect_ms": 155.5, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.4023798005655408}]}
{"time": "2026-10-01T20:03:55.392392-04:00", "frame": 67, "detect_ms": 155.7, "detections": [{"x": 38, "y": 81, "width": 82, "height": 159, "score": 0.4797950249630958}]}
{"time": "2026-10-01T20:03:55.551073-04:00", "frame": 68, "detect_ms": 156.1, "detections": [{"x": 35, "y": 77, "width": 85, "height": 163, "score": 0.21222396520897746}]}
{"time": "2026-10-01T20:03:55.710597-04:00", "frame": 69, "detect_ms": 156.1, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.4049730247352272}]}
{"time": "2026-10-01T20:03:55.869218-04:00", "frame": 70, "detect_ms": 156.0, "detections": [{"x": 36, "y": 78, "width": 84, "height": 162, "score": 0.1630471555981785}]}
{"time": "2026-10-01T20:03:56.027815-04:00", "frame": 71, "detect_ms": 155.8, "detections": [{"x": 38, "y": 81, "width": 82, "height": 159, "score": 0.41790558444336057}]}
{"time": "2026-10-01T20:03:56.186330-04:00", "frame": 72, "detect_ms": 155.9, "detections": [{"x": 38, "y": 81, "width": 82, "height": 159, "score": 0.3997681778855622}]}
{"time": "2026-10-01T20:03:56.345329-04:00", "frame": 73, "detect_ms": 156.2, "detections": [{"x": 40, "y": 81, "width": 82, "height": 159, "score": 0.3769234602805227}]}
{"time": "2026-10-01T20:03:56.504353-04:00", "frame": 74, "detect_ms": 156.4, "detections": [{"x": 40, "y": 83, "width": 81, "height": 157, "score": 0.3991089353803545}]}
{"time": "2026-10-01T20:03:56.662470-04:00", "frame": 75, "detect_ms": 155.5, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.38614989642519504}]}
{"time": "2026-10-01T20:03:56.820769-04:00", "frame": 76, "detect_ms": 155.8, "detections": [{"x": 38, "y": 81, "width": 82, "height": 159, "score": 0.2709786770865321}]}
{"time": "2026-10-01T20:03:56.980050-04:00", "frame": 77, "detect_ms": 156.8, "detections": [{"x": 36, "y": 75, "width": 86, "height": 165, "score": 0.6518183110747486}]}
{"time": "2026-10-01T20:03:57.139304-04:00", "frame": 78, "detect_ms": 156.6, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.28632063907571137}]}
{"time": "2026-10-01T20:03:57.298335-04:00", "frame": 79, "detect_ms": 155.4, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.43558389792451635}]}
{"time": "2026-10-01T20:03:57.456706-04:00", "frame": 80, "detect_ms": 155.8, "detections": [{"x": 40, "y": 83, "width": 81, "height": 157, "score": 0.8634709393372759}]}
{"time": "2026-10-01T20:03:57.615992-04:00", "frame": 81, "detect_ms": 156.5, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.4331272724084556}]}
{"time": "2026-10-01T20:03:57.775399-04:00", "frame": 82, "detect_ms": 156.5, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.49840413546189666}]}
{"time": "2026-10-01T20:03:57.933771-04:00", "frame": 83, "detect_ms": 155.7, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.32624540221877396}]}
{"time": "2026-10-01T20:03:58.091998-04:00", "frame": 84, "detect_ms": 155.7, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.5480134333483875}]}
{"time": "2026-10-01T20:03:58.250202-04:00", "frame": 85, "detect_ms": 155.7, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.5704890991328284}]}
{"time": "2026-10-01T20:03:58.409720-04:00", "frame": 86, "detect_ms": 156.1, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.6839129338040948}]}
{"time": "2026-10-01T20:03:58.568246-04:00", "frame": 87, "detect_ms": 155.9, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.8485558265820146}]}
{"time": "2026-10-01T20:03:58.726275-04:00", "frame": 88, "detect_ms": 155.4, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 1.000986670027487}]}
{"time": "2026-10-01T20:03:58.884950-04:00", "frame": 89, "detect_ms": 156.2, "detections": [{"x": 38, "y": 70, "width": 86, "height": 170, "score": 1.3886284687323496}]}
{"time": "2026-10-01T20:03:59.044294-04:00", "frame": 90, "detect_ms": 156.6, "detections": [{"x": 38, "y": 68, "width": 88, "height": 172, "score": 1.3258185563609004}]}
{"time": "2026-10-01T20:03:59.203541-04:00", "frame": 91, "detect_ms": 156.4, "detections": [{"x": 40, "y": 67, "width": 87, "height": 173, "score": 0.24236263683997095}]}
{"time": "2026-10-01T20:03:59.361403-04:00", "frame": 92, "detect_ms": 155.2, "detections": [{"x": 42, "y": 68, "width": 89, "height": 172, "score": 0.4422684946330264}]}
{"time": "2026-10-01T20:03:59.519999-04:00", "frame": 93, "detect_ms": 156.1, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.2364501968258992}]}
{"time": "2026-10-01T20:03:59.678600-04:00", "frame": 94, "detect_ms": 156.1, "detections": [{"x": 46, "y": 68, "width": 86, "height": 172, "score": 1.1815928664291278}]}
{"time": "2026-10-01T20:03:59.837681-04:00", "frame": 95, "detect_ms": 156.4, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.2039410008583218}]}
{"time": "2026-10-01T20:03:59.996262-04:00", "frame": 96, "detect_ms": 155.9, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.2542638191953301}]}
{"time": "2026-10-01T20:04:00.154635-04:00", "frame": 97, "detect_ms": 155.8, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 0.7811401297803968}]}
{"time": "2026-10-01T20:04:00.313476-04:00", "frame": 98, "detect_ms": 156.3, "detections": [{"x": 42, "y": 68, "width": 88, "height": 172, "score": 0.5225869874702767}]}
{"time": "2026-10-01T20:04:00.472446-04:00", "frame": 99, "detect_ms": 156.3, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.0299179576104507}]}
{"time": "2026-10-01T20:04:00.630733-04:00", "frame": 100, "detect_ms": 155.6, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.387091884156689}]}
{"time": "2026-10-01T20:04:00.789314-04:00", "frame": 101, "detect_ms": 155.8, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.3055211084429175}]}
{"time": "2026-10-01T20:04:00.947883-04:00", "frame": 102, "detect_ms": 156.1, "detections": [{"x": 44, "y": 66, "width": 88, "height": 174, "score": 1.4084361975546926}]}
{"time": "2026-10-01T20:04:01.107641-04:00", "frame": 103, "detect_ms": 156.2, "detections": [{"x": 42, "y": 68, "width": 88, "height": 172, "score": 0.7735100549180061}]}
{"time": "2026-10-01T20:04:01.266140-04:00", "frame": 104, "detect_ms": 155.8, "detections": [{"x": 45, "y": 68, "width": 86, "height": 172, "score": 1.0843693704809994}]}
{"time": "2026-10-01T20:04:01.424456-04:00", "frame": 105, "detect_ms": 155.6, "detections": [{"x": 46, "y": 68, "width": 86, "height": 172, "score": 1.039136976818554}]}
{"time": "2026-10-01T20:04:01.583742-04:00", "frame": 106, "detect_ms": 156.8, "detections": [{"x": 44, "y": 66, "width": 88, "height": 174, "score": 0.7808800623752177}]}
{"time": "2026-10-01T20:04:01.743021-04:00", "frame": 107, "detect_ms": 156.5, "detections": [{"x": 44, "y": 66, "width": 88, "height": 174, "score": 1.1076241738628596}]}
{"time": "2026-10-01T20:04:01.902200-04:00", "frame": 108, "detect_ms": 156.5, "detections": [{"x": 44, "y": 66, "width": 88, "height": 174, "score": 1.0421022255904973}]}
{"time": "2026-10-01T20:04:02.060585-04:00", "frame": 109, "detect_ms": 155.8, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.1500556468963623}]}
{"time": "2026-10-01T20:04:02.219021-04:00", "frame": 110, "detect_ms": 155.9, "detections": [{"x": 46, "y": 68, "width": 86, "height": 172, "score": 1.4070313512347639}]}
{"time": "2026-10-01T20:04:02.377940-04:00", "frame": 111, "detect_ms": 156.2, "detections": [{"x": 46, "y": 68, "width": 86, "height": 172, "score": 1.296724711311981}]}
{"time": "2026-10-01T20:04:02.536688-04:00", "frame": 112, "detect_ms": 156.1, "detections": [{"x": 46, "y": 71, "width": 86, "height": 169, "score": 1.0493187201209366}]}
{"time": "2026-10-01T20:04:02.695255-04:00", "frame": 113, "detect_ms": 155.9, "detections": [{"x": 46, "y": 70, "width": 86, "height": 170, "score": 1.4308511454146355}]}
{"time": "2026-10-01T20:04:02.853991-04:00", "frame": 114, "detect_ms": 156.1, "detections": [{"x": 46, "y": 71, "width": 84, "height": 169, "score": 1.3316734116524458}]}
{"time": "2026-10-01T20:04:03.012551-04:00", "frame": 115, "detect_ms": 156.1, "detections": [{"x": 46, "y": 71, "width": 86, "height": 169, "score": 1.4532637507654727}]}
{"time": "2026-10-01T20:04:03.171668-04:00", "frame": 116, "detect_ms": 156.1, "detections": [{"x": 43, "y": 67, "width": 88, "height": 173, "score": 1.3160466596018523}]}
{"time": "2026-10-01T20:04:03.329625-04:00", "frame": 117, "detect_ms": 155.3, "detections": [{"x": 44, "y": 66, "width": 88, "height": 174, "score": 0.543132537510246}]}
{"time": "2026-10-01T20:04:03.487858-04:00", "frame": 118, "detect_ms": 155.7, "detections": [{"x": 39, "y": 72, "width": 88, "height": 168, "score": 0.4426002739928663}]}
{"time": "2026-10-01T20:04:03.646359-04:00", "frame": 119, "detect_ms": 156.0, "detections": [{"x": 39, "y": 75, "width": 86, "height": 165, "score": 0.7297875457443297}]}
{"time": "2026-10-01T20:04:03.805635-04:00", "frame": 120, "detect_ms": 156.3, "detections": [{"x": 35, "y": 77, "width": 85, "height": 163, "score": 0.26965864514932036}]}
{"time": "2026-10-01T20:04:03.964370-04:00", "frame": 121, "detect_ms": 155.9, "detections": [{"x": 33, "y": 72, "width": 88, "height": 168, "score": 0.132359562208876}]}
{"time": "2026-10-01T20:04:04.122695-04:00", "frame": 122, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:04:04.281365-04:00", "frame": 123, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:04:04.440218-04:00", "frame": 124, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:04:04.598708-04:00", "frame": 125, "detect_ms": 155.8, "detections": [{"x": 51, "y": 88, "width": 74, "height": 148, "score": 0.37889132532291114}]}
{"time": "2026-10-01T20:04:04.756915-04:00", "frame": 126, "detect_ms": 155.5, "detections": [{"x": 57, "y": 81, "width": 76, "height": 153, "score": 0.6354650820139796}]}
+76
View File
@@ -0,0 +1,76 @@
{"time": "2026-10-01T20:27:13.366750-04:00", "frame": 1, "detect_ms": 218.1, "detections": []}
{"time": "2026-10-01T20:27:13.532726-04:00", "frame": 2, "detect_ms": 155.7, "detections": []}
{"time": "2026-10-01T20:27:13.691215-04:00", "frame": 3, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:13.850076-04:00", "frame": 4, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:14.010001-04:00", "frame": 5, "detect_ms": 156.9, "detections": []}
{"time": "2026-10-01T20:27:14.168423-04:00", "frame": 6, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:27:14.326515-04:00", "frame": 7, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:14.484950-04:00", "frame": 8, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:27:14.644893-04:00", "frame": 9, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:27:14.803741-04:00", "frame": 10, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:14.968957-04:00", "frame": 11, "detect_ms": 161.6, "detections": []}
{"time": "2026-10-01T20:27:15.127086-04:00", "frame": 12, "detect_ms": 155.6, "detections": []}
{"time": "2026-10-01T20:27:15.285533-04:00", "frame": 13, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:27:15.444739-04:00", "frame": 14, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:27:15.603841-04:00", "frame": 15, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:27:15.762328-04:00", "frame": 16, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:27:15.920560-04:00", "frame": 17, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:16.079227-04:00", "frame": 18, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:16.238433-04:00", "frame": 19, "detect_ms": 156.6, "detections": []}
{"time": "2026-10-01T20:27:16.396556-04:00", "frame": 20, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:16.555447-04:00", "frame": 21, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:16.714330-04:00", "frame": 22, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:27:16.875731-04:00", "frame": 23, "detect_ms": 158.7, "detections": []}
{"time": "2026-10-01T20:27:17.034417-04:00", "frame": 24, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:17.192481-04:00", "frame": 25, "detect_ms": 155.6, "detections": []}
{"time": "2026-10-01T20:27:17.351070-04:00", "frame": 26, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:17.510704-04:00", "frame": 27, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:17.669458-04:00", "frame": 28, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:17.827123-04:00", "frame": 29, "detect_ms": 155.1, "detections": []}
{"time": "2026-10-01T20:27:17.985227-04:00", "frame": 30, "detect_ms": 155.7, "detections": []}
{"time": "2026-10-01T20:27:18.144303-04:00", "frame": 31, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:27:18.303047-04:00", "frame": 32, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:18.461024-04:00", "frame": 33, "detect_ms": 155.4, "detections": []}
{"time": "2026-10-01T20:27:18.619194-04:00", "frame": 34, "detect_ms": 155.7, "detections": []}
{"time": "2026-10-01T20:27:18.778007-04:00", "frame": 35, "detect_ms": 156.5, "detections": []}
{"time": "2026-10-01T20:27:18.937022-04:00", "frame": 36, "detect_ms": 156.3, "detections": [{"x": 250, "y": 0, "width": 69, "height": 130, "score": 0.10211979364976287}]}
{"time": "2026-10-01T20:27:19.095129-04:00", "frame": 37, "detect_ms": 155.4, "detections": []}
{"time": "2026-10-01T20:27:19.253516-04:00", "frame": 38, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:19.412717-04:00", "frame": 39, "detect_ms": 156.8, "detections": []}
{"time": "2026-10-01T20:27:19.571769-04:00", "frame": 40, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:19.730327-04:00", "frame": 41, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:19.888584-04:00", "frame": 42, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:20.046985-04:00", "frame": 43, "detect_ms": 155.9, "detections": []}
{"time": "2026-10-01T20:27:20.206469-04:00", "frame": 44, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:20.365206-04:00", "frame": 45, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:20.523344-04:00", "frame": 46, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:20.681876-04:00", "frame": 47, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:20.841400-04:00", "frame": 48, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:21.000215-04:00", "frame": 49, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:21.158266-04:00", "frame": 50, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:21.316925-04:00", "frame": 51, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:21.475967-04:00", "frame": 52, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:21.634565-04:00", "frame": 53, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:27:21.792683-04:00", "frame": 54, "detect_ms": 155.6, "detections": []}
{"time": "2026-10-01T20:27:21.951147-04:00", "frame": 55, "detect_ms": 156.0, "detections": []}
{"time": "2026-10-01T20:27:22.109606-04:00", "frame": 56, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:22.268476-04:00", "frame": 57, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:22.426802-04:00", "frame": 58, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:22.585129-04:00", "frame": 59, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:22.744682-04:00", "frame": 60, "detect_ms": 157.1, "detections": []}
{"time": "2026-10-01T20:27:22.903878-04:00", "frame": 61, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:23.062252-04:00", "frame": 62, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:23.220458-04:00", "frame": 63, "detect_ms": 155.7, "detections": []}
{"time": "2026-10-01T20:27:23.378678-04:00", "frame": 64, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:23.537648-04:00", "frame": 65, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:23.696024-04:00", "frame": 66, "detect_ms": 155.8, "detections": []}
{"time": "2026-10-01T20:27:23.854253-04:00", "frame": 67, "detect_ms": 155.7, "detections": []}
{"time": "2026-10-01T20:27:24.012749-04:00", "frame": 68, "detect_ms": 156.1, "detections": []}
{"time": "2026-10-01T20:27:24.171854-04:00", "frame": 69, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:24.330052-04:00", "frame": 70, "detect_ms": 155.6, "detections": []}
{"time": "2026-10-01T20:27:24.489027-04:00", "frame": 71, "detect_ms": 156.3, "detections": []}
{"time": "2026-10-01T20:27:24.647864-04:00", "frame": 72, "detect_ms": 156.4, "detections": []}
{"time": "2026-10-01T20:27:24.806802-04:00", "frame": 73, "detect_ms": 156.2, "detections": []}
{"time": "2026-10-01T20:27:24.965784-04:00", "frame": 74, "detect_ms": 156.3, "detections": [{"x": 250, "y": 0, "width": 69, "height": 130, "score": 0.05548620340414345}]}
{"time": "2026-10-01T20:27:25.123778-04:00", "frame": 75, "detect_ms": 155.5, "detections": []}
{"time": "2026-10-01T20:27:25.282098-04:00", "frame": 76, "detect_ms": 155.9, "detections": []}
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+65
View File
@@ -0,0 +1,65 @@
# First eye firmware test
Local Wi-Fi OTA support is deployed and the first network update succeeded on both eye boards and the camera on 2026-09-30. Board 2 continued receiving gaze packets afterward; Board 1 resumed receiving them after Joshua fixed its cable. See [OTA setup and update steps](ota.md).
`eye_test/` is a sketch for the two Waveshare ESP32-S3-Touch-LCD-2.1 Rev2.0 boards. It uses the display, touch, I2C, and expander drivers copied from Waveshare's official `LVGL_Arduino` example (source and SHA-256 recorded in [`vendor/README.md`](../vendor/README.md)). `EyeScene.cpp` draws the Freddy eye and continues moving and blinking without camera input. The on-screen board number makes each upload visually identifiable. The wired gaze receivers on both boards now receive the camera's UART signal and visibly track together.
## Wired gaze build — 2026-09-27
The camera broadcasts the [versioned UART gaze frame](../protocol/gaze.md) to both boards. Each eye receives on GPIO44 through its 4-pin UART header, validates the frame and sequence, eases toward the target, and returns to autonomous idle movement after an invalid target or 750 ms of silence. Each eye shows `TRACK` or `IDLE` on screen and prints `GAZE_READY`, `GAZE_ACCEPT`, `GAZE_REJECT`, and `GAZE_TIMEOUT` over USB CDC for the bench log. The separate UART USB-C port must be unplugged for the header to work. Since 2026-10-01, `TRACK` also selects the red-eye appearance and `IDLE` restores blue.
## Automatic red eye on tracking — 2026-10-01
Joshua chose confirmed person tracking rather than a distance threshold as the red-eye trigger. Both eye variants now use the same validated camera `valid` flag that controls gaze and the `TRACK`/`IDLE` label. A valid target switches to the existing dark eye with a pulsing red center; an invalid target or the receiver's 750 ms signal timeout restores the blue eye. Gaze easing and blinking continue in either appearance. The old touch-toggle test overlay was removed, so touching a display no longer changes its color. The camera firmware and gaze protocol did not change.
Board 1 image: 1,304,432 bytes, SHA-256 `60dd759971f3c19c05a40e6b7d33d92702549f03cc83e849e9647e5fac6a7280`. Board 2 image: 1,304,432 bytes, SHA-256 `f134ecd75fd64c4d3f494d9e1d98ccf3493abfb5e40c45ba6391c5e6724bd4`. Both compiled with their distinct board labels and target markers, accepted a Wi-Fi OTA update, rebooted, and responded from slot 0. The camera remained online in slot 1. The automatic visual transition awaits Joshua's on-device check. Persistent empty-scene false detections can still trigger the red appearance; outdoor false-positive testing remains necessary.
Both numbered variants were built with hardware USB CDC logging and flashed to their MAC-checked boards on 2026-09-27; all upload image hashes verified. Board 1 (`…CF:F8`): 1,253,667 bytes program, 96,760 bytes static RAM, application SHA-256 `ea0bd68c6fba802d89d11fdee1674c0ac66a376475cacd4681917afcb0bc560b`. Board 2 (`…D2:10`): 1,253,667 bytes program, 96,760 bytes static RAM, application SHA-256 `8cb6564ed46754efa7cd707d7ca219dbcbf69cc77fa7ad6adeac752203c29288`. After the UART wiring was connected, both accepted matching camera coordinates and Joshua confirmed both moved together in `TRACK`. A physical diagnostic also proved both reject duplicate, older, wrong-version, and bad-CRC frames, then recover. See [the bench log](../hardware/gaze-link-bench.md).
## Freddy iris texture — 2026-09-21
The user's [`official_freddy_eye_texture.webp`](../reference_images/official_freddy_eye_texture.webp) is the iris artwork. [`make_iris.py`](tools/make_iris.py) crops the painted disc, scales it to the 178 × 178 pixel iris, adds a feathered transparent edge over the source's white backing, and encodes it as an LVGL RGB565-plus-alpha image in [`FreddyIris.cpp`](eye_test/FreddyIris.cpp). The generated [iris preview](assets/freddy_iris_preview.png) shows the image used by the firmware; the [whole-eye preview](assets/freddy_eye_display_preview.png) shows its centered position against the sclera. The original WebP is unchanged. To regenerate, run `python firmware/tools/make_iris.py` with Pillow available. See [eye-style.md](../assets/eye-style.md) for the visual baseline.
`EyeScene.cpp` moves this image in place of the original flat-color iris and separately drawn pupil/highlight. The cream sclera, dark outer ring, 4.6-second blink cycle, and 30 ms gaze-animation timer are unchanged. The source image already contains the pupil and highlights. Source SHA-256: `0d686ed397b15e1417ffbe2dcb5e6a426e16dfd4fbeda2f4e28bd975fea8229f`.
The board settings are ESP32S3 Dev Module, 16 MB flash, OPI PSRAM, and a 3 MB application partition. The compile-only command is:
```sh
arduino-cli compile \
--fqbn esp32:esp32:esp32s3:FlashSize=16M,PSRAM=opi,PartitionScheme=app3M_fat9M_16MB,USBMode=hwcdc,CDCOnBoot=cdc \
--libraries vendor/extracted/Arduino/libraries \
--build-path firmware/build-eye-test \
--build-property runtime.tools.ctags.path="$PWD/vendor/ctags-tool/bin" \
--build-property runtime.tools.ctags-5.8-arduino11.path="$PWD/vendor/ctags-tool/bin" \
--jobs 4 firmware/eye_test
```
For Board 2, add `--build-property compiler.cpp.extra_flags=-DEYE_BOARD_NUMBER=2` and use a separate build path (`firmware/build-eye-test-board2`). The default is Board 1. Do not override `build.extra_flags`; that removes the ESP32-S3 USB CDC definitions and silences Board 2's USB console. Both variants use the same eye scene and animation.
## Board 1 upload — 2026-09-21
The sketch compiled successfully: 1,123,899 bytes program storage (35% of a 3 MB app partition) and 96,404 bytes static RAM (29%). The application binary SHA-256 is `3d30b8df10c95b72877ef3c7a0a5a9fdcf50a4c22f77372dffa7be8a7ff758f0`.
The connected board was confirmed by hardware-address suffix `CF:F8` as Board 1. `arduino-cli upload` at 460800 baud wrote the bootloader, partition table, boot-app selector, and eye application to `/dev/cu.usbmodem1101`; esptool verified the hash of each written image and reset the board. Joshua confirmed the eyeball visibly looks around, blinks, and fits cleanly within the round display. Achieved frame rate has not yet been measured.
A direct read of Board 1's full flash stopped with a serial stream error before completing, so no byte-for-byte device backup exists. The official Waveshare full 16 MB stock image was extracted to `firmware/backups/waveshare-stock-2026-09-21.bin` as a recovery option (SHA-256 `3653fb24cbc8e4874e2cd43f170585b15e30bc434f086ba99082d2da17e644d4`). It may differ from the exact image originally shipped on this unit. The local backup file is ignored by Git. See `hardware/bringup.md` for board identity and display observations.
## Board 2 upload — 2026-09-21
Board 2 was reidentified by hardware-address suffix `D2:10`. Its labeled variant compiled successfully: 1,152,363 bytes program storage (36%) and 96,396 bytes static RAM (29%). The application binary contains `BOARD 2 EYE TEST` and has SHA-256 `27ab300069d7f48c82aacd50e667b9b4472260d2c197c382dcdad944e8fd80c7`. `arduino-cli upload --verify` at 460800 baud verified all four written images and reset the board. Joshua confirmed the label, looking-around motion, and clean blinking on screen.
## Textured firmware test — 2026-09-21
Both board variants compile with the embedded artwork. Board 1's new application is 1,218,915 bytes (38%); its binary SHA-256 is `77f12720866622a9f4fbe19d84830517db650221a00bfb601ca0eef74965242d`. Board 2's is 1,247,375 bytes (39%); SHA-256 `2e9584a0d292f9eb7da3d3216f069bb537309c9760ba712bc7ecd8c8c6a7fc33`.
Board 2 (`D2:10`) was flashed with the new build and all four image hashes verified. Joshua confirmed that the textured iris displays, moves, and blinks correctly. Board 1 still runs the previous flat-color test until its textured build is flashed.
## Eyelid color revision — 2026-09-21
At Joshua's request, the moving eyelid objects and thin perimeter ring now use `#743824`; the surrounding background remains `#39271D`. The generated [open-eye preview](assets/freddy_eye_display_preview.png) and [mid-blink preview](assets/freddy_eye_blink_preview.png) show the matching color against the textured iris. Both board variants recompiled. The current Board 1 binary SHA-256 is `f7135765fad65a4dab936e981b08b7766ca480b9a16f250809df1bc68636b7e2`; Board 2 is `150a2460a1cab5e73034f3f8dadce6095774348e9ea0913996cd621f37d5a2bd`. Board 2 was reidentified as `D2:10`, flashed, and all four image hashes verified. Board 1 (`CF:F8`) was then connected on `/dev/cu.usbmodem2101`, flashed with its matching build, and all four written image hashes verified. Joshua confirmed both displays show the textured blue iris and matching brown lids/ring while looking around and blinking.
## Glowing-red bench preview — 2026-09-21
`EyeScene_SetProximityRed(bool)` switches the active eye from the blue iris and cream sclera to a dark moving iris/sclera with a pulsating red center inspired by [`freddy_glowing_red_pupils.png`](../reference_images/freddy_glowing_red_pupils.png). The same gaze movement and blink lids remain active. The [red preview](assets/freddy_eye_red_preview.png) approximates the display composition. For bench testing, tap an eye screen to toggle its state; each board toggles independently until the proximity message link exists.
Both labeled variants compiled and were hash-verified after upload on their identity-checked boards (`CF:F8` on `/dev/cu.usbmodem2101`, `D2:10` on `/dev/cu.usbmodem1101`). Board 1 uses 1,219,503 bytes of program storage (38%) and 96,420 bytes static RAM (29%); Board 2 uses 1,248,019 bytes (39%) and 96,412 bytes (29%). Joshua confirmed that a tap switches both boards to the dark eye with a glowing red center while motion and blinking continue, and a second tap returns each to blue. The measured two-foot trigger has not been implemented; see [the proximity plan](../planning/proximity-red.md).
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+18
View File
@@ -0,0 +1,18 @@
# TFmini Plus distance bench
This is a receive-only sketch for a DFRobot DFR1154 ESP32-S3 camera board and the owned TFmini Plus. It **compiles but has not been flashed or run**. It reads the sensor's default 9-byte UART frames, checks the checksum and signal strength, and reports distance and a range-only `near` flag over USB serial. It does not perform person detection or change either eye. The TF-Luna uses the same default frame, but this sketch's 10 cm minimum must be changed to 20 cm before using it with that sensor.
Before wiring, confirm the actual camera board revision and sensor cable against the [DFRobot board documentation](https://wiki.dfrobot.com/dfr1154/) and [Benewake TFmini Plus manual](https://en.benewake.com/uploadfiles/2025/04/20250430175221028.pdf). The manual's stock cable lists red +5V, white RXD, green TXD, and black GND. Verify the cable on the unit at hand before applying power. The intended receive-only connection is:
| TFmini Plus | DFR1154 bench connection |
| --- | --- |
| +5V | Regulated 5 V supply, sized for at least the manual's 140 mA peak |
| GND | Supply ground joined to DFR1154 ground |
| TXD | Exposed DFR1154 GPIO44 / RXD (3.3 V UART signal) |
| RXD | Leave disconnected for this read-only test |
Do not use the camera board's 3.3 V pin to supply the TFmini Plus; the manual specifies 5 V ±0.5 V. The sketch assumes the sensor still has its factory UART setting of 115200 baud and 9-byte frames in centimeters. It rejects bad checksums, signal strength below 100 or overexposure, and distances under the 10 cm rated minimum. It reports `near=1` at or below 61 cm and returns to `near=0` at or above 76 cm or after 300 ms without a valid reading. Those thresholds are bench settings; the final effect also needs person confirmation and synchronized messages to both eyes.
The compile-only board profile used on this laptop is `esp32:esp32:esp32s3:FlashSize=16M,PSRAM=opi,PartitionScheme=app3M_fat9M_16MB,USBMode=hwcdc,CDCOnBoot=cdc` with Arduino ESP32 core 3.3.5. The build uses 322,359 bytes program storage and 21,360 bytes static RAM. The DFR1154's actual flash/PSRAM and USB configuration must be checked before upload. Uploading this sketch would replace whatever camera firmware is currently installed; save its recovery path first.
For a bench run, record the printed distance and strength at approximately 10, 20, 45, 60, and 75 cm, a very-close position, several off-axis positions, and when a nearby object is present without a person. Record observations in `tracking/proximity-bench.md` and decide whether the narrow beam and mounting position are acceptable.
@@ -0,0 +1,96 @@
// Benewake TFmini Plus receive-only bench test for the DFRobot DFR1154 camera.
// Sensor default: 9-byte/cm frames at 115200 baud. Confirm the exact board
// revision and sensor cable pinout before wiring or flashing.
//
// TFmini Plus needs regulated 5 V (+/-0.5 V), not the 3.3 V Gravity pin.
// Join supply and DFR1154 grounds. Connect sensor TXD to camera GPIO44/RXD.
// Sensor RXD is not needed. The TF-Luna uses the same default frame, but its
// minimum rated distance is 20 cm rather than the 10 cm used below.
#include <Arduino.h>
namespace {
constexpr int kLidarRxPin = 44;
constexpr int kUnusedLidarTxPin = -1;
constexpr uint32_t kLidarBaud = 115200;
constexpr uint16_t kNearEnterCm = 61;
constexpr uint16_t kNearExitCm = 76;
constexpr uint32_t kReportPeriodMs = 100;
HardwareSerial lidar(1);
uint8_t frame[9] = {};
uint8_t frame_index = 0;
uint16_t distance_cm = 0;
uint16_t strength = 0;
bool valid_range = false;
bool near_range = false;
uint32_t last_valid_ms = 0;
uint32_t last_report_ms = 0;
void accept_frame() {
uint8_t sum = 0;
for (int i = 0; i < 8; ++i) sum += frame[i];
if (sum != frame[8]) return;
distance_cm = static_cast<uint16_t>(frame[2]) |
(static_cast<uint16_t>(frame[3]) << 8);
strength = static_cast<uint16_t>(frame[4]) |
(static_cast<uint16_t>(frame[5]) << 8);
valid_range = strength >= 100 && strength != 0xFFFF && distance_cm >= 10;
// This is a range-only preview. The final red state also requires a
// camera-confirmed person and both eye boards receiving the same message.
if (valid_range) {
last_valid_ms = millis();
if (!near_range && distance_cm <= kNearEnterCm) near_range = true;
if (near_range && distance_cm >= kNearExitCm) near_range = false;
}
}
void accept_byte(uint8_t value) {
if (frame_index == 0) {
if (value == 0x59) frame[frame_index++] = value;
return;
}
if (frame_index == 1) {
if (value == 0x59) {
frame[frame_index++] = value;
} else {
frame_index = 0;
}
return;
}
frame[frame_index++] = value;
if (frame_index == sizeof(frame)) {
accept_frame();
frame_index = 0;
}
}
} // namespace
void setup() {
Serial.begin(115200);
lidar.begin(kLidarBaud, SERIAL_8N1, kLidarRxPin, kUnusedLidarTxPin);
delay(250);
Serial.println("TFmini Plus receive-only bench: GPIO44/RXD, 115200, cm frames");
}
void loop() {
while (lidar.available()) accept_byte(static_cast<uint8_t>(lidar.read()));
const uint32_t now = millis();
if (now - last_valid_ms > 300) {
valid_range = false;
near_range = false;
}
if (now - last_report_ms >= kReportPeriodMs) {
last_report_ms = now;
if (valid_range) {
Serial.printf("distance_cm=%u strength=%u near=%u\n", distance_cm,
strength, near_range ? 1 : 0);
} else {
Serial.println("range=invalid_or_waiting");
}
}
}
+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()
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(freddy_camera_person)
+21
View File
@@ -0,0 +1,21 @@
# On-device person-position experiment
This ESP-IDF project combines the DFR1154 camera pin map with Espressif's [pedestrian detection model](https://github.com/espressif/esp-dl/blob/master/models/pedestrian_detect/README.md). It prints timestamped person centers and bounding boxes over USB and broadcasts [gaze frames](../../protocol/gaze.md) from GPIO43 to the two eye boards. Physical delivery and synchronized eye motion are bench-verified.
The project contains local Wi-Fi OTA support. Its `partitions.csv` has two 7 MB app slots and its bootloader enables rollback. The one-time USB migration and first OTA update succeeded on 2026-09-30. A later USB flash on 2026-10-01 installed the tracking filter in slot 0; see the [tracking check](../../hardware/tracking-filter-2026-10-01.md) and [OTA procedure](../ota.md). OTA cannot change the partition table itself.
## Tracking filter — 2026-10-01
After a clearly empty 20-second view yielded 15 apparent person boxes, the camera now rejects top-edge boxes at least 95% of image height. All 15 archived empty-scene boxes fit this geometry; none of 203 matched held-person detections in earlier center/left/right logs did. Once three consistent detections confirm a target, `tracking::Gate` sends the last confirmed position for up to one second through brief model misses or a new unconfirmed candidate. A persistent miss returns `valid=0`. The camera still sends the same version-1 UART frame to both eyes; each eye uses `valid=1` for red/track and `valid=0` for blue/idle. The change compiled, passed its native state-transition test, and was MAC-checked and USB-flashed with verified hashes. The current application SHA-256 is `76d16c8817db9e9568eaf2485a486a0debaa2687fc5a8447954d0cd0d80d468a`.
In the same empty view after flashing, the camera sent **0 valid targets in 80 frames** and recorded 15 rejected full-height candidates. Both eyes accepted the same invalid states for all 78 overlapping logged sequences. A close real person might also fill the image height, so test this filter with a visitor at the intended near/middle/far approach positions before accepting it for the porch. [Raw captures and interpretation](../../hardware/tracking-filter-2026-10-01.md).
On 2026-09-27, the gaze build was flashed to the camera after its MAC `E8:F6:0A:87:0B:4C` was checked. Following a temporary malformed-frame diagnostic build, the normal camera application (SHA-256 `0acc763624b20a09afbf512e20afad51ffc9de4276102ffa639a0345d39d70a5`) was restored and hash-verified after flashing. Both eyes accepted matching camera coordinates; Joshua confirmed synchronized visible tracking. The [bench log](../../hardware/gaze-link-bench.md) records rejection of malformed/stale commands and recovery. A live USB sample showed 231–232 ms capture-to-result time and `gaze_valid` transitions. The current three-consecutive-frame gate rejects isolated detections, and the camera rejects detections whose boxes span at least 90% of both image dimensions. Other persistent false detections can still pass; this is not finished target selection.
The project builds with ESP-IDF v5.5.2 and was hash-verified after flashing to the camera identified by MAC `E8:F6:0A:87:0B:4C`. Its first 20-second live serial sample at the model's default 0.70 score threshold reported `NO_PERSON` at 320 × 240. A subsequent run at threshold 0.35, with better lighting and a person in view, produced stable timestamped bounding boxes at left, center, and right held positions. The corresponding logs and JPEGs are in `../../hardware/camera-captures/`, summarized in `../../hardware/bringup.md`. Typical capture-to-result latency was 233 ms (about four results per second). Espressif reports about 118 ms model time for a 224 × 224 input on an ESP32-S3; the measured figure here includes capture and JPEG decode. Camera pins match DFRobot's [CameraWebServer example](https://github.com/DFRobot/DFR1154_Examples/blob/master/5.3%20CameraWebServer/source%20code/5_3/5_3.ino). Dependencies are pinned to compatible release ranges in `main/idf_component.yml`; `dependencies.lock` records the resolved versions. Diagnostic JPEG output is disabled by default; enable it with `FREDDY_CAMERA_DIAGNOSTIC_JPEG=1` for captures.
The current build also drives GPIO47 high at boot to enable the board's infrared illumination, as identified in [DFRobot's DFR1154 guide](https://wiki.dfrobot.com/dfr1154/). The camera must be aimed clear of nearby surfaces: an IR-lit frame captured close to a surface washed out into a purple blur, while a re-aimed frame showed the room. This has not yet established better detection in darkness, and the raw model reports some false positives in an empty room. Diagnostic JPEG output is disabled by default in the gaze build and can be re-enabled with `FREDDY_CAMERA_DIAGNOSTIC_JPEG=1` for captures.
When diagnostic JPEG output is enabled, use `capture_diagnostic.py` with PySerial to record synchronized logs and JPEGs. Test continuous walking, dim and brighter lighting, a close person, a distant person, two people, and a person crossing the camera edge before relying on the coordinates for gaze. Improve target persistence and false-positive rejection beyond the preliminary three-frame gate. The DFRobot bundled `Person_Detection_inferencing.zip` is only a 96 × 96 binary classifier (`non_person` versus `person`), so it cannot provide the required position directly.
On 2026-09-27 Joshua reported intermittent person lock. A temporary diagnostic image showed the camera aimed mostly at the ceiling/window, with only part of him at the far left edge; the model alternated `PERSON` and `NO_PERSON` and reported left-edge boxes. See [the diagnosis and paired frames](../../hardware/tracking-drop-diagnosis.md). The normal build above was restored and verified; first reframe the camera, then retest before tuning tracking hysteresis.
@@ -0,0 +1,29 @@
Executing action: all (aliases: build)
Running ninja in directory /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build
Executing "ninja all"...
[1/9] Performing build step for 'bootloader'
[1/1] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/bootloader.bin
Bootloader binary size 0x5240 bytes. 0x2dc0 bytes (36%) free.
[2/9] No install step for 'bootloader'
[3/9] Completed 'bootloader'
[4/9] Building CXX object esp-idf/main/CMakeFiles/__idf_main.dir/main.cpp.obj
[5/9] Linking C static library esp-idf/main/libmain.a
[6/9] Generating esp-idf/esp_system/ld/sections.ld
[7/9] Linking CXX executable freddy_camera_person.elf
[8/9] Generating binary image from built executable
esptool.py v4.12.dev1
Creating esp32s3 image...
Merged 2 ELF sections
Successfully created esp32s3 image.
Generated /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
[9/9] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/partition_table/partition-table.bin /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
freddy_camera_person.bin binary size 0x288bd0 bytes. Smallest app partition is 0xa00000 bytes. 0x777430 bytes (75%) free.
Project build complete. To flash, run:
idf.py flash
or
idf.py -p PORT flash
or
python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m 0x0 firmware/camera_person_idf/build/bootloader/bootloader.bin 0x8000 firmware/camera_person_idf/build/partition_table/partition-table.bin 0x10000 firmware/camera_person_idf/build/freddy_camera_person.bin
or from the "/Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build" directory
python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"
@@ -0,0 +1,29 @@
Executing action: all (aliases: build)
Running ninja in directory /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build
Executing "ninja all"...
[1/9] Performing build step for 'bootloader'
[1/1] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/bootloader.bin
Bootloader binary size 0x5240 bytes. 0x2dc0 bytes (36%) free.
[2/9] No install step for 'bootloader'
[3/9] Completed 'bootloader'
[4/9] Building CXX object esp-idf/main/CMakeFiles/__idf_main.dir/main.cpp.obj
[5/9] Linking C static library esp-idf/main/libmain.a
[6/9] Generating esp-idf/esp_system/ld/sections.ld
[7/9] Linking CXX executable freddy_camera_person.elf
[8/9] Generating binary image from built executable
esptool.py v4.12.dev1
Creating esp32s3 image...
Merged 2 ELF sections
Successfully created esp32s3 image.
Generated /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
[9/9] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/partition_table/partition-table.bin /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
freddy_camera_person.bin binary size 0x288e00 bytes. Smallest app partition is 0xa00000 bytes. 0x777200 bytes (75%) free.
Project build complete. To flash, run:
idf.py flash
or
idf.py -p PORT flash
or
python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m 0x0 firmware/camera_person_idf/build/bootloader/bootloader.bin 0x8000 firmware/camera_person_idf/build/partition_table/partition-table.bin 0x10000 firmware/camera_person_idf/build/freddy_camera_person.bin
or from the "/Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build" directory
python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"
@@ -0,0 +1,8 @@
I (120984) freddy_person: PERSON t_ms=120020 score=0.800 cx=207 I (364804) freddy_person: NO_PERSON t_ms=363838 frame=320,240 latency_ms=230
I (366054) freddy_person: PERSON t_ms=365088 score=0.370 cx=165 cy=173 box=136,134,194,212 frame=320,240 latency_ms=231
I (366304) freddy_person: PERSON t_ms=365338 score=0.455 cx=174 cy=174 box=154,138,195,211 frame=320,240 latency_ms=231
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-empty-frame-1441.jpg (4499 bytes)
I (370644) freddy_person: NO_PERSON t_ms=369678 frame=320,240 latency_ms=230
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-empty-frame-1461.jpg (4550 bytes)
I (373734) freddy_person: PERSON t_ms=372773 score=0.375 cx=153 cy=175 box=135,137,171,213 frame=320,240 latency_ms=326
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-empty.log; 4 person detections
@@ -0,0 +1,23 @@
I (123) esp_image: segment 1: paddr=00093494 vaddr=3fc98900 sizeI (42934) freddy_person: PERSON t_ms=41969 score=0.500 cx=293 cy=209 box=267,180,319,239 frame=320,240 latency_ms=232
I (43184) freddy_person: PERSON t_ms=42219 score=0.515 cx=293 cy=211 box=267,183,319,239 frame=320,240 latency_ms=231
I (43434) freddy_person: PERSON t_ms=42468 score=0.364 cx=294 cy=209 box=269,180,319,239 frame=320,240 latency_ms=230
I (43684) freddy_person: PERSON t_ms=42718 score=0.375 cx=294 cy=210 box=269,183,319,238 frame=320,240 latency_ms=231
I (44184) freddy_person: PERSON t_ms=43218 score=0.364 cx=293 cy=210 box=267,181,319,239 frame=320,240 latency_ms=231
I (44434) freddy_person: PERSON t_ms=43468 score=0.354 cx=305 cy=212 box=292,186,319,239 frame=320,240 latency_ms=231
I (44684) freddy_person: PERSON t_ms=43719 score=0.395 cx=159 cy=119 box=0,0,319,239 frame=320,240 latency_ms=231
I (44934) freddy_person: PERSON t_ms=43968 score=0.415 cx=293 cy=209 box=268,180,319,239 frame=320,240 latency_ms=231
I (45184) freddy_person: PERSON t_ms=44219 score=0.500 cx=293 cy=209 box=268,181,319,238 frame=320,240 latency_ms=231
I (45684) freddy_person: PERSON t_ms=44718 score=0.364 cx=160 cy=119 box=1,0,319,239 frame=320,240 latency_ms=231
I (46434) freddy_person: PERSON t_ms=45469 score=0.459 cx=159 cy=119 box=0,0,319,239 frame=320,240 latency_ms=231
I (46684) freddy_person: PERSON t_ms=45720 score=0.580 cx=159 cy=119 box=0,0,319,239 frame=320,240 latency_ms=233
I (47424) freddy_person: PERSON t_ms=46457 score=0.395 cx=215 cy=119 box=112,0,319,239 frame=320,240 latency_ms=230
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-empty-frame-181.jpg (2941 bytes)
I (47734) freddy_person: PERSON t_ms=46769 score=0.385 cx=215 cy=119 box=111,0,319,239 frame=320,240 latency_ms=291
I (47984) freddy_person: PERSON t_ms=47018 score=0.405 cx=213 cy=119 box=108,0,319,239 frame=320,240 latency_ms=230
I (48234) freddy_person: PERSON t_ms=47268 score=0.484 cx=217 cy=119 box=115,0,319,239 frame=320,240 latency_ms=230
I (48984) freddy_person: PERSON t_ms=48018 score=0.468 cx=214 cy=119 box=110,0,319,239 frame=320,240 latency_ms=230
I (49224) freddy_person: PERSON t_ms=48267 score=0.364 cx=211 cy=119 box=104,0,319,239 frame=320,240 latency_ms=229
I (49984) freddy_person: PERSON t_ms=49018 score=0.459 cx=213 cy=119 box=107,0,319,239 frame=320,240 latency_ms=230
I (50234) freddy_person: PERSON t_ms=49267 score=0.415 cx=213 cy=119 box=108,0,319,239 frame=320,240 latency_ms=230
I (50484) freddy_person: PERSON t_ms=49517 score=0.405 cx=213 cy=119 box=107,0,319,239 frame=320,240 latency_ms=230
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-empty.log; 21 person detections
@@ -0,0 +1,28 @@
I (179914) freddy_person: PERSON t_ms=178950 score=0.661 cx=34 cy=99 box=0,0,68,199 frame=320,240 latency_ms=232
I (180164) freddy_person: PERSON t_ms=179200 score=0.530 cx=32 cy=98 box=0,0,64,196 frame=320,240 latency_ms=232
I (180414) freddy_person: PERSON t_ms=179449 score=0.459 cx=30 cy=99 box=0,0,60,198 frame=320,240 latency_ms=232
I (180664) freddy_person: PERSON t_ms=179700 score=0.573 cx=28 cy=99 box=0,1,56,197 frame=320,240 latency_ms=232
I (180914) freddy_person: PERSON t_ms=179949 score=0.530 cx=26 cy=98 box=0,1,52,196 frame=320,240 latency_ms=232
I (181414) freddy_person: PERSON t_ms=180450 score=0.559 cx=29 cy=97 box=0,0,59,195 frame=320,240 latency_ms=232
I (181664) freddy_person: PERSON t_ms=180700 score=0.625 cx=29 cy=97 box=0,0,58,194 frame=320,240 latency_ms=232
I (182164) freddy_person: PERSON t_ms=181199 score=0.459 cx=28 cy=97 box=0,0,56,194 frame=320,240 latency_ms=232
I (182414) freddy_person: PERSON t_ms=181449 score=0.385 cx=99 cy=133 box=58,85,140,182 frame=320,240 latency_ms=231
I (182664) freddy_person: PERSON t_ms=181700 score=0.492 cx=27 cy=93 box=0,0,55,187 frame=320,240 latency_ms=232
I (182914) freddy_person: PERSON t_ms=181950 score=0.612 cx=29 cy=97 box=0,1,59,194 frame=320,240 latency_ms=232
I (183164) freddy_person: PERSON t_ms=182200 score=0.580 cx=34 cy=96 box=0,0,68,193 frame=320,240 latency_ms=232
I (183414) freddy_person: PERSON t_ms=182449 score=0.395 cx=40 cy=119 box=0,1,81,238 frame=320,240 latency_ms=232
I (183914) freddy_person: PERSON t_ms=182949 score=0.385 cx=44 cy=91 box=0,0,88,182 frame=320,240 latency_ms=232
I (184164) freddy_person: PERSON t_ms=183199 score=0.375 cx=49 cy=119 box=0,0,99,238 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-reaimed-frame-721.jpg (5539 bytes)
I (185024) freddy_person: NO_PERSON t_ms=184059 frame=320,240 latency_ms=231
I (185274) freddy_person: PERSON t_ms=184309 score=0.395 cx=48 cy=119 box=0,0,96,239 frame=320,240 latency_ms=232
I (185774) freddy_person: PERSON t_ms=184810 score=0.612 cx=38 cy=101 box=0,0,76,202 frame=320,240 latency_ms=232
I (186024) freddy_person: PERSON t_ms=185060 score=0.650 cx=34 cy=101 box=0,0,68,203 frame=320,240 latency_ms=232
I (186274) freddy_person: PERSON t_ms=185310 score=0.538 cx=26 cy=106 box=0,1,53,211 frame=320,240 latency_ms=232
I (186524) freddy_person: PERSON t_ms=185560 score=0.545 cx=25 cy=134 box=0,51,50,217 frame=320,240 latency_ms=233
I (186774) freddy_person: PERSON t_ms=185810 score=0.395 cx=26 cy=104 box=0,3,52,206 frame=320,240 latency_ms=232
I (187024) freddy_person: PERSON t_ms=186060 score=0.459 cx=24 cy=132 box=0,47,49,217 frame=320,240 latency_ms=232
I (187274) freddy_person: PERSON t_ms=186310 score=0.459 cx=23 cy=138 box=0,55,46,221 frame=320,240 latency_ms=232
I (187524) freddy_person: PERSON t_ms=186560 score=0.707 cx=25 cy=134 box=0,46,50,222 frame=320,240 latency_ms=232
I (187774) freddy_person: PERSON t_ms=186810 score=0.538 cx=23 cy=132 box=0,54,46,210 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-reaimed.log; 25 person detections
@@ -0,0 +1,13 @@
I (50974) freddy_person: PERSON t_ms=50017 score=0.375 cx=215 cyI (73984) freddy_person: NO_PERSON t_ms=73027 frame=320,240 latency_ms=229
I (74234) freddy_person: PERSON t_ms=73277 score=0.364 cx=212 cy=119 box=105,0,319,239 frame=320,240 latency_ms=229
I (74494) freddy_person: PERSON t_ms=73527 score=0.385 cx=216 cy=119 box=113,0,319,239 frame=320,240 latency_ms=230
I (75984) freddy_person: PERSON t_ms=75027 score=0.375 cx=216 cy=117 box=113,0,319,235 frame=320,240 latency_ms=229
I (76494) freddy_person: PERSON t_ms=75527 score=0.424 cx=214 cy=119 box=110,0,319,239 frame=320,240 latency_ms=230
I (76734) freddy_person: PERSON t_ms=75777 score=0.385 cx=214 cy=119 box=110,0,319,239 frame=320,240 latency_ms=229
I (77244) freddy_person: PERSON t_ms=76277 score=0.424 cx=214 cy=119 box=110,0,319,239 frame=320,240 latency_ms=230
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-settled-frame-301.jpg (2942 bytes)
I (79794) freddy_person: PERSON t_ms=78837 score=0.385 cx=213 cy=119 box=107,0,319,239 frame=320,240 latency_ms=229
I (80554) freddy_person: PERSON t_ms=79588 score=0.433 cx=214 cy=119 box=109,0,319,239 frame=320,240 latency_ms=230
I (80804) freddy_person: PERSON t_ms=79838 score=0.405 cx=214 cy=119 box=110,0,319,239 frame=320,240 latency_ms=230
I (81044) freddy_person: PERSON t_ms=80087 score=0.375 cx=215 cy=119 box=111,0,319,239 frame=320,240 latency_ms=229
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-ir-settled.log; 11 person detections
@@ -0,0 +1,33 @@
I (41804) freddy_person: PERSON t_ms=40840 score=0.586 cx=166 cyI (98354) freddy_person: PERSON t_ms=97390 score=0.515 cx=215 cy=119 box=111,0,319,239 frame=320,240 latency_ms=233
I (98854) freddy_person: PERSON t_ms=97890 score=0.484 cx=217 cy=120 box=122,4,313,237 frame=320,240 latency_ms=233
I (99104) freddy_person: PERSON t_ms=98140 score=0.593 cx=220 cy=123 box=121,8,319,239 frame=320,240 latency_ms=232
I (99354) freddy_person: PERSON t_ms=98390 score=0.599 cx=237 cy=119 box=156,0,319,239 frame=320,240 latency_ms=232
I (99604) freddy_person: PERSON t_ms=98640 score=0.515 cx=234 cy=120 box=149,2,319,239 frame=320,240 latency_ms=232
I (99854) freddy_person: PERSON t_ms=98889 score=0.468 cx=250 cy=131 box=181,23,319,239 frame=320,240 latency_ms=232
I (100104) freddy_person: PERSON t_ms=99139 score=0.433 cx=230 cy=139 box=142,39,319,239 frame=320,240 latency_ms=232
I (100354) freddy_person: PERSON t_ms=99390 score=0.484 cx=229 cy=139 box=139,40,319,239 frame=320,240 latency_ms=232
I (100604) freddy_person: PERSON t_ms=99640 score=0.530 cx=260 cy=141 box=201,44,319,239 frame=320,240 latency_ms=232
I (100854) freddy_person: PERSON t_ms=99890 score=0.476 cx=257 cy=142 box=196,46,319,238 frame=320,240 latency_ms=233
I (101104) freddy_person: PERSON t_ms=100139 score=0.545 cx=218 cy=142 box=120,46,316,239 frame=320,240 latency_ms=232
I (101354) freddy_person: PERSON t_ms=100390 score=0.484 cx=234 cy=142 box=150,45,319,239 frame=320,240 latency_ms=232
I (101604) freddy_person: PERSON t_ms=100640 score=0.468 cx=258 cy=143 box=197,48,319,239 frame=320,240 latency_ms=232
I (101854) freddy_person: PERSON t_ms=100890 score=0.637 cx=230 cy=143 box=141,48,319,239 frame=320,240 latency_ms=232
I (102104) freddy_person: PERSON t_ms=101140 score=0.566 cx=241 cy=143 box=163,48,319,239 frame=320,240 latency_ms=232
I (102354) freddy_person: PERSON t_ms=101390 score=0.723 cx=236 cy=144 box=154,50,319,239 frame=320,240 latency_ms=232
I (102604) freddy_person: PERSON t_ms=101640 score=0.776 cx=228 cy=146 box=138,53,319,239 frame=320,240 latency_ms=232
I (102854) freddy_person: PERSON t_ms=101890 score=0.690 cx=243 cy=145 box=167,52,319,239 frame=320,240 latency_ms=232
I (103104) freddy_person: PERSON t_ms=102140 score=0.637 cx=238 cy=145 box=158,52,319,239 frame=320,240 latency_ms=232
I (103354) freddy_person: PERSON t_ms=102390 score=0.612 cx=232 cy=143 box=147,48,317,239 frame=320,240 latency_ms=232
I (103604) freddy_person: PERSON t_ms=102640 score=0.702 cx=224 cy=144 box=129,49,319,239 frame=320,240 latency_ms=232
I (103854) freddy_person: PERSON t_ms=102890 score=0.791 cx=230 cy=144 box=141,49,319,239 frame=320,240 latency_ms=232
I (104104) freddy_person: PERSON t_ms=103140 score=0.612 cx=225 cy=145 box=132,51,319,239 frame=320,240 latency_ms=232
I (104354) freddy_person: PERSON t_ms=103390 score=0.786 cx=222 cy=144 box=126,50,319,239 frame=320,240 latency_ms=232
I (104604) freddy_person: PERSON t_ms=103640 score=0.834 cx=227 cy=144 box=136,50,319,239 frame=320,240 latency_ms=232
I (104854) freddy_person: PERSON t_ms=103890 score=0.734 cx=222 cy=143 box=126,48,319,239 frame=320,240 latency_ms=232
I (105104) freddy_person: PERSON t_ms=104140 score=0.723 cx=231 cy=143 box=145,48,318,239 frame=320,240 latency_ms=233
I (105354) freddy_person: PERSON t_ms=104390 score=0.745 cx=219 cy=143 box=120,48,319,239 frame=320,240 latency_ms=232
I (105604) freddy_person: PERSON t_ms=104639 score=0.586 cx=219 cy=145 box=121,51,317,239 frame=320,240 latency_ms=232
I (105854) freddy_person: PERSON t_ms=104889 score=0.740 cx=221 cy=145 box=123,52,319,239 frame=320,240 latency_ms=232
I (106104) freddy_person: PERSON t_ms=105140 score=0.702 cx=251 cy=146 box=183,54,319,239 frame=320,240 latency_ms=232
I (106354) freddy_person: PERSON t_ms=105390 score=0.593 cx=250 cy=147 box=185,55,316,239 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-left.log; 32 person detections
@@ -0,0 +1,52 @@
I (50984) freddy_person: PERSON t_ms=50020 score=0.848 cx=61 cy=I (108564) freddy_person: PERSON t_ms=107600 score=0.723 cx=214 cy=138 box=123,37,306,239 frame=320,240 latency_ms=233
I (108814) freddy_person: PERSON t_ms=107851 score=0.810 cx=216 cy=137 box=123,35,310,239 frame=320,240 latency_ms=233
I (109064) freddy_person: PERSON t_ms=108101 score=0.901 cx=219 cy=138 box=128,40,311,237 frame=320,240 latency_ms=233
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-left-image-frame-421.jpg (6708 bytes)
I (109454) freddy_person: PERSON t_ms=108490 score=0.707 cx=210 cy=139 box=119,39,301,239 frame=320,240 latency_ms=373
I (109704) freddy_person: PERSON t_ms=108740 score=0.888 cx=218 cy=139 box=120,39,316,239 frame=320,240 latency_ms=233
I (109954) freddy_person: PERSON t_ms=108990 score=0.734 cx=214 cy=131 box=125,41,304,221 frame=320,240 latency_ms=233
I (110204) freddy_person: PERSON t_ms=109241 score=0.839 cx=209 cy=139 box=123,41,296,238 frame=320,240 latency_ms=233
I (110454) freddy_person: PERSON t_ms=109490 score=0.834 cx=214 cy=137 box=128,40,301,235 frame=320,240 latency_ms=233
I (110704) freddy_person: PERSON t_ms=109740 score=0.824 cx=217 cy=139 box=123,40,312,239 frame=320,240 latency_ms=233
I (110954) freddy_person: PERSON t_ms=109990 score=0.713 cx=219 cy=139 box=132,40,307,239 frame=320,240 latency_ms=233
I (111204) freddy_person: PERSON t_ms=110240 score=0.834 cx=214 cy=139 box=127,39,302,239 frame=320,240 latency_ms=233
I (111454) freddy_person: PERSON t_ms=110490 score=0.755 cx=220 cy=138 box=130,39,310,238 frame=320,240 latency_ms=233
I (111704) freddy_person: PERSON t_ms=110740 score=0.839 cx=218 cy=140 box=126,41,310,239 frame=320,240 latency_ms=233
I (111954) freddy_person: PERSON t_ms=110991 score=0.599 cx=215 cy=138 box=118,37,312,239 frame=320,240 latency_ms=233
I (112204) freddy_person: PERSON t_ms=111241 score=0.888 cx=218 cy=139 box=128,39,308,239 frame=320,240 latency_ms=233
I (112454) freddy_person: PERSON t_ms=111490 score=0.786 cx=221 cy=138 box=128,40,314,237 frame=320,240 latency_ms=233
I (112704) freddy_person: PERSON t_ms=111740 score=0.702 cx=217 cy=139 box=127,40,308,239 frame=320,240 latency_ms=233
I (112954) freddy_person: PERSON t_ms=111990 score=0.829 cx=222 cy=139 box=128,40,317,238 frame=320,240 latency_ms=233
I (113204) freddy_person: PERSON t_ms=112240 score=0.884 cx=220 cy=138 box=131,38,309,239 frame=320,240 latency_ms=233
I (113454) freddy_person: PERSON t_ms=112491 score=0.631 cx=212 cy=138 box=122,41,302,235 frame=320,240 latency_ms=233
I (113704) freddy_person: PERSON t_ms=112740 score=0.897 cx=219 cy=139 box=126,39,312,239 frame=320,240 latency_ms=233
I (113954) freddy_person: PERSON t_ms=112990 score=0.786 cx=221 cy=137 box=132,39,310,236 frame=320,240 latency_ms=233
I (114204) freddy_person: PERSON t_ms=113240 score=0.815 cx=208 cy=136 box=127,39,290,234 frame=320,240 latency_ms=233
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-left-image-frame-441.jpg (6659 bytes)
I (114594) freddy_person: PERSON t_ms=113629 score=0.566 cx=217 cy=137 box=130,38,304,237 frame=320,240 latency_ms=372
I (114844) freddy_person: PERSON t_ms=113881 score=0.530 cx=204 cy=138 box=102,40,307,237 frame=320,240 latency_ms=233
I (115094) freddy_person: PERSON t_ms=114130 score=0.740 cx=206 cy=135 box=127,41,286,230 frame=320,240 latency_ms=233
I (115344) freddy_person: PERSON t_ms=114381 score=0.690 cx=212 cy=136 box=129,39,295,233 frame=320,240 latency_ms=233
I (115594) freddy_person: PERSON t_ms=114630 score=0.791 cx=202 cy=136 box=125,38,279,235 frame=320,240 latency_ms=233
I (115844) freddy_person: PERSON t_ms=114880 score=0.723 cx=208 cy=136 box=127,38,290,235 frame=320,240 latency_ms=233
I (116094) freddy_person: PERSON t_ms=115130 score=0.820 cx=213 cy=138 box=125,38,302,239 frame=320,240 latency_ms=233
I (116344) freddy_person: PERSON t_ms=115380 score=0.573 cx=206 cy=136 box=130,39,282,233 frame=320,240 latency_ms=233
I (116594) freddy_person: PERSON t_ms=115630 score=0.745 cx=208 cy=134 box=126,39,291,230 frame=320,240 latency_ms=233
I (116844) freddy_person: PERSON t_ms=115881 score=0.786 cx=203 cy=138 box=122,39,285,238 frame=320,240 latency_ms=233
I (117094) freddy_person: PERSON t_ms=116130 score=0.875 cx=218 cy=138 box=128,37,308,239 frame=320,240 latency_ms=233
I (117344) freddy_person: PERSON t_ms=116380 score=0.879 cx=220 cy=138 box=129,37,312,239 frame=320,240 latency_ms=233
I (117594) freddy_person: PERSON t_ms=116630 score=0.820 cx=220 cy=138 box=124,38,316,239 frame=320,240 latency_ms=233
I (117844) freddy_person: PERSON t_ms=116880 score=0.810 cx=205 cy=136 box=121,38,290,235 frame=320,240 latency_ms=232
I (118094) freddy_person: PERSON t_ms=117131 score=0.755 cx=211 cy=138 box=119,38,304,238 frame=320,240 latency_ms=233
I (118344) freddy_person: PERSON t_ms=117380 score=0.702 cx=215 cy=137 box=124,36,306,239 frame=320,240 latency_ms=233
I (118594) freddy_person: PERSON t_ms=117630 score=0.679 cx=214 cy=138 box=125,37,304,239 frame=320,240 latency_ms=233
I (118844) freddy_person: PERSON t_ms=117880 score=0.824 cx=214 cy=138 box=128,38,300,239 frame=320,240 latency_ms=233
I (119094) freddy_person: PERSON t_ms=118130 score=0.631 cx=192 cy=134 box=80,37,305,232 frame=320,240 latency_ms=232
I (119344) freddy_person: PERSON t_ms=118381 score=0.637 cx=218 cy=138 box=119,38,318,239 frame=320,240 latency_ms=233
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-left-image-frame-461.jpg (6661 bytes)
I (119734) freddy_person: PERSON t_ms=118769 score=0.810 cx=223 cy=139 box=128,39,319,239 frame=320,240 latency_ms=371
I (119984) freddy_person: PERSON t_ms=119021 score=0.530 cx=219 cy=135 box=129,37,310,233 frame=320,240 latency_ms=233
I (120234) freddy_person: PERSON t_ms=119271 score=0.857 cx=217 cy=138 box=122,38,313,239 frame=320,240 latency_ms=233
I (120484) freddy_person: PERSON t_ms=119520 score=0.786 cx=199 cy=137 box=122,38,276,237 frame=320,240 latency_ms=233
I (120734) freddy_person: PERSON t_ms=119770 score=0.707 cx=204 cy=132 box=124,39,285,225 frame=320,240 latency_ms=233
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-left-image.log; 48 person detections
@@ -0,0 +1,34 @@
I (106604) freddy_person: PERSON t_ms=105639 score=0.805 cx=218 I (133904) freddy_person: PERSON t_ms=132940 score=0.476 cx=35 cy=140 box=0,45,70,236 frame=320,240 latency_ms=232
I (134154) freddy_person: PERSON t_ms=133190 score=0.552 cx=240 cy=119 box=162,0,318,239 frame=320,240 latency_ms=233
I (134404) freddy_person: PERSON t_ms=133440 score=0.530 cx=242 cy=119 box=167,0,318,239 frame=320,240 latency_ms=233
I (134654) freddy_person: PERSON t_ms=133690 score=0.566 cx=38 cy=140 box=0,43,77,237 frame=320,240 latency_ms=233
I (134904) freddy_person: PERSON t_ms=133941 score=0.538 cx=235 cy=119 box=152,0,319,239 frame=320,240 latency_ms=233
I (135154) freddy_person: PERSON t_ms=134190 score=0.667 cx=240 cy=119 box=162,0,319,239 frame=320,240 latency_ms=233
I (135404) freddy_person: PERSON t_ms=134440 score=0.580 cx=42 cy=140 box=1,41,83,239 frame=320,240 latency_ms=233
I (135654) freddy_person: PERSON t_ms=134691 score=0.552 cx=40 cy=139 box=0,39,81,239 frame=320,240 latency_ms=233
I (135904) freddy_person: PERSON t_ms=134941 score=0.723 cx=235 cy=119 box=151,0,319,239 frame=320,240 latency_ms=233
I (136154) freddy_person: PERSON t_ms=135190 score=0.545 cx=234 cy=119 box=150,0,319,239 frame=320,240 latency_ms=233
I (136404) freddy_person: PERSON t_ms=135440 score=0.679 cx=47 cy=139 box=0,40,94,239 frame=320,240 latency_ms=233
I (136654) freddy_person: PERSON t_ms=135690 score=0.468 cx=43 cy=139 box=0,39,87,239 frame=320,240 latency_ms=233
I (136904) freddy_person: PERSON t_ms=135941 score=0.515 cx=42 cy=137 box=0,35,85,239 frame=320,240 latency_ms=233
I (137154) freddy_person: PERSON t_ms=136190 score=0.523 cx=52 cy=138 box=0,38,105,239 frame=320,240 latency_ms=233
I (137404) freddy_person: PERSON t_ms=136441 score=0.586 cx=238 cy=119 box=157,0,319,239 frame=320,240 latency_ms=234
I (137654) freddy_person: PERSON t_ms=136691 score=0.656 cx=47 cy=138 box=0,37,95,239 frame=320,240 latency_ms=233
I (137904) freddy_person: PERSON t_ms=136940 score=0.650 cx=235 cy=119 box=152,0,319,239 frame=320,240 latency_ms=233
I (138154) freddy_person: PERSON t_ms=137191 score=0.661 cx=236 cy=119 box=154,0,318,239 frame=320,240 latency_ms=233
I (138404) freddy_person: PERSON t_ms=137441 score=0.559 cx=49 cy=135 box=1,32,97,239 frame=320,240 latency_ms=233
I (138654) freddy_person: PERSON t_ms=137691 score=0.740 cx=235 cy=119 box=152,0,318,239 frame=320,240 latency_ms=233
I (138904) freddy_person: PERSON t_ms=137941 score=0.643 cx=237 cy=119 box=155,0,319,239 frame=320,240 latency_ms=233
I (139154) freddy_person: PERSON t_ms=138191 score=0.523 cx=241 cy=119 box=164,0,319,239 frame=320,240 latency_ms=233
I (139404) freddy_person: PERSON t_ms=138440 score=0.538 cx=237 cy=119 box=156,0,319,239 frame=320,240 latency_ms=233
I (139654) freddy_person: PERSON t_ms=138690 score=0.552 cx=240 cy=119 box=162,0,318,239 frame=320,240 latency_ms=233
I (139904) freddy_person: PERSON t_ms=138941 score=0.667 cx=236 cy=119 box=153,0,319,239 frame=320,240 latency_ms=233
I (140154) freddy_person: PERSON t_ms=139191 score=0.661 cx=235 cy=119 box=152,0,319,239 frame=320,240 latency_ms=233
I (140404) freddy_person: PERSON t_ms=139440 score=0.643 cx=240 cy=119 box=161,0,319,239 frame=320,240 latency_ms=232
I (140654) freddy_person: PERSON t_ms=139691 score=0.508 cx=243 cy=119 box=167,0,319,239 frame=320,240 latency_ms=233
I (140904) freddy_person: PERSON t_ms=139940 score=0.734 cx=239 cy=119 box=160,0,319,239 frame=320,240 latency_ms=232
I (141154) freddy_person: PERSON t_ms=140191 score=0.612 cx=241 cy=119 box=163,0,319,239 frame=320,240 latency_ms=233
I (141404) freddy_person: PERSON t_ms=140440 score=0.707 cx=237 cy=119 box=156,0,318,239 frame=320,240 latency_ms=233
I (141654) freddy_person: PERSON t_ms=140690 score=0.696 cx=240 cy=119 box=161,0,319,239 frame=320,240 latency_ms=233
I (141904) freddy_person: PERSON t_ms=140940 score=0.734 cx=239 cy=119 box=161,0,318,239 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-right.log; 33 person detections
@@ -0,0 +1,51 @@
I (123) esp_image: segment 1: paddr=000933e4 vaddr=3fc98900 sizeI (38744) freddy_person: PERSON t_ms=37780 score=0.862 cx=68 cy=145 box=0,51,136,239 frame=320,240 latency_ms=233
I (38994) freddy_person: PERSON t_ms=38031 score=0.810 cx=61 cy=145 box=0,51,123,239 frame=320,240 latency_ms=233
I (39244) freddy_person: PERSON t_ms=38281 score=0.824 cx=63 cy=143 box=0,48,127,239 frame=320,240 latency_ms=233
I (39494) freddy_person: PERSON t_ms=38530 score=0.650 cx=62 cy=144 box=0,49,124,239 frame=320,240 latency_ms=233
I (39744) freddy_person: PERSON t_ms=38781 score=0.661 cx=63 cy=144 box=0,50,127,239 frame=320,240 latency_ms=233
I (39994) freddy_person: PERSON t_ms=39030 score=0.795 cx=65 cy=143 box=0,48,131,239 frame=320,240 latency_ms=233
I (40244) freddy_person: PERSON t_ms=39280 score=0.707 cx=63 cy=142 box=0,46,127,239 frame=320,240 latency_ms=233
I (40494) freddy_person: PERSON t_ms=39531 score=0.834 cx=66 cy=143 box=0,48,133,239 frame=320,240 latency_ms=233
I (40744) freddy_person: PERSON t_ms=39781 score=0.843 cx=61 cy=143 box=1,48,122,239 frame=320,240 latency_ms=233
I (40994) freddy_person: PERSON t_ms=40030 score=0.745 cx=63 cy=143 box=0,48,126,239 frame=320,240 latency_ms=233
I (41244) freddy_person: PERSON t_ms=40280 score=0.829 cx=64 cy=144 box=0,50,129,239 frame=320,240 latency_ms=232
I (41494) freddy_person: PERSON t_ms=40530 score=0.755 cx=60 cy=143 box=0,47,120,239 frame=320,240 latency_ms=232
I (41744) freddy_person: PERSON t_ms=40780 score=0.765 cx=62 cy=142 box=0,46,124,239 frame=320,240 latency_ms=233
I (41994) freddy_person: PERSON t_ms=41030 score=0.839 cx=63 cy=143 box=0,47,127,239 frame=320,240 latency_ms=232
I (42244) freddy_person: PERSON t_ms=41279 score=0.897 cx=61 cy=140 box=0,41,123,239 frame=320,240 latency_ms=232
I (42494) freddy_person: PERSON t_ms=41530 score=0.919 cx=61 cy=139 box=0,39,123,239 frame=320,240 latency_ms=233
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-right-image-frame-161.jpg (5650 bytes)
I (42864) freddy_person: PERSON t_ms=41899 score=0.862 cx=61 cy=140 box=0,42,123,239 frame=320,240 latency_ms=351
I (43114) freddy_person: PERSON t_ms=42150 score=0.884 cx=63 cy=141 box=0,44,126,239 frame=320,240 latency_ms=233
I (43364) freddy_person: PERSON t_ms=42400 score=0.888 cx=62 cy=141 box=0,44,124,239 frame=320,240 latency_ms=233
I (43614) freddy_person: PERSON t_ms=42649 score=0.829 cx=64 cy=140 box=0,42,128,239 frame=320,240 latency_ms=232
I (43864) freddy_person: PERSON t_ms=42900 score=0.820 cx=64 cy=141 box=0,43,128,239 frame=320,240 latency_ms=232
I (44114) freddy_person: PERSON t_ms=43150 score=0.910 cx=63 cy=141 box=0,43,126,239 frame=320,240 latency_ms=232
I (44364) freddy_person: PERSON t_ms=43400 score=0.884 cx=63 cy=141 box=0,43,126,239 frame=320,240 latency_ms=232
I (44614) freddy_person: PERSON t_ms=43650 score=0.910 cx=62 cy=140 box=0,42,124,239 frame=320,240 latency_ms=232
I (44864) freddy_person: PERSON t_ms=43900 score=0.901 cx=62 cy=141 box=0,44,124,239 frame=320,240 latency_ms=233
I (45114) freddy_person: PERSON t_ms=44150 score=0.888 cx=61 cy=140 box=0,42,123,239 frame=320,240 latency_ms=232
I (45364) freddy_person: PERSON t_ms=44400 score=0.871 cx=62 cy=141 box=0,44,124,239 frame=320,240 latency_ms=232
I (45614) freddy_person: PERSON t_ms=44650 score=0.781 cx=62 cy=141 box=0,44,124,239 frame=320,240 latency_ms=232
I (45864) freddy_person: PERSON t_ms=44900 score=0.852 cx=62 cy=141 box=1,43,124,239 frame=320,240 latency_ms=232
I (46114) freddy_person: PERSON t_ms=45151 score=0.879 cx=63 cy=141 box=0,44,127,239 frame=320,240 latency_ms=233
I (46364) freddy_person: PERSON t_ms=45401 score=0.914 cx=64 cy=141 box=0,44,128,239 frame=320,240 latency_ms=233
I (46614) freddy_person: PERSON t_ms=45651 score=0.906 cx=62 cy=142 box=0,46,125,239 frame=320,240 latency_ms=233
I (46864) freddy_person: PERSON t_ms=45900 score=0.884 cx=62 cy=142 box=0,46,125,239 frame=320,240 latency_ms=232
I (47114) freddy_person: PERSON t_ms=46150 score=0.871 cx=65 cy=143 box=0,47,131,239 frame=320,240 latency_ms=232
I (47364) freddy_person: PERSON t_ms=46400 score=0.848 cx=62 cy=142 box=0,45,124,239 frame=320,240 latency_ms=233
I (47614) freddy_person: PERSON t_ms=46650 score=0.734 cx=62 cy=141 box=0,44,124,239 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-right-image-frame-181.jpg (5545 bytes)
I (47974) freddy_person: PERSON t_ms=47017 score=0.834 cx=63 cy=142 box=0,46,127,239 frame=320,240 latency_ms=349
I (48234) freddy_person: PERSON t_ms=47271 score=0.857 cx=61 cy=142 box=0,46,122,239 frame=320,240 latency_ms=233
I (48484) freddy_person: PERSON t_ms=47520 score=0.857 cx=64 cy=142 box=0,45,128,239 frame=320,240 latency_ms=232
I (48734) freddy_person: PERSON t_ms=47770 score=0.750 cx=63 cy=143 box=1,47,125,239 frame=320,240 latency_ms=232
I (48984) freddy_person: PERSON t_ms=48020 score=0.871 cx=62 cy=142 box=0,46,125,239 frame=320,240 latency_ms=233
I (49234) freddy_person: PERSON t_ms=48270 score=0.871 cx=61 cy=142 box=0,46,122,239 frame=320,240 latency_ms=233
I (49484) freddy_person: PERSON t_ms=48519 score=0.857 cx=62 cy=143 box=0,47,124,239 frame=320,240 latency_ms=232
I (49734) freddy_person: PERSON t_ms=48770 score=0.805 cx=62 cy=142 box=0,46,124,239 frame=320,240 latency_ms=233
I (49984) freddy_person: PERSON t_ms=49020 score=0.786 cx=62 cy=142 box=0,45,125,239 frame=320,240 latency_ms=233
I (50234) freddy_person: PERSON t_ms=49270 score=0.857 cx=62 cy=142 box=1,46,123,239 frame=320,240 latency_ms=233
I (50484) freddy_person: PERSON t_ms=49520 score=0.866 cx=61 cy=142 box=0,45,123,239 frame=320,240 latency_ms=233
I (50734) freddy_person: PERSON t_ms=49770 score=0.820 cx=61 cy=142 box=0,45,123,239 frame=320,240 latency_ms=232
Saved /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/hardware/camera-captures/person-detector-right-image.log; 48 person detections
@@ -0,0 +1,64 @@
"""Record serial detections and reconstruct diagnostic JPEGs from the camera."""
import argparse
import pathlib
import time
import serial
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", default="/dev/cu.usbmodem1101")
parser.add_argument("--seconds", type=float, default=30)
parser.add_argument("--name", default="person-detector-diagnostic")
args = parser.parse_args()
output = pathlib.Path(__file__).resolve().parents[2] / "hardware/camera-captures"
output.mkdir(parents=True, exist_ok=True)
log_path = output / f"{args.name}.log"
frame_number = None
expected_length = 0
hex_lines: list[str] = []
detections = 0
lines: list[str] = []
with serial.Serial(args.port, 115200, timeout=0.5) as port:
deadline = time.monotonic() + args.seconds
while time.monotonic() < deadline:
raw = port.readline()
if not raw:
continue
line = raw.decode("utf-8", "replace").strip()
lines.append(line)
if "freddy_person: PERSON " in line:
detections += 1
print(line, flush=True)
elif "freddy_person: NO_PERSON " in line and len(lines) % 20 == 0:
print(line, flush=True)
if line.startswith("FRAME_BEGIN "):
_, number, length = line.split()
frame_number = int(number)
expected_length = int(length)
hex_lines = []
elif line.startswith("FRAME_HEX ") and frame_number is not None:
hex_lines.append(line.removeprefix("FRAME_HEX "))
elif line.startswith("FRAME_END ") and frame_number is not None:
number = int(line.split()[1])
if number == frame_number:
data = bytes.fromhex("".join(hex_lines))
if len(data) == expected_length and data.startswith(b"\xff\xd8") and data.endswith(b"\xff\xd9"):
photo = output / f"{args.name}-frame-{number}.jpg"
photo.write_bytes(data)
print(f"Saved {photo} ({len(data)} bytes)", flush=True)
else:
print(f"Incomplete frame {number}: {len(data)}/{expected_length} bytes", flush=True)
frame_number = None
log_path.write_text("\n".join(lines) + "\n")
print(f"Saved {log_path}; {detections} person detections", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,103 @@
dependencies:
espressif/dl_fft:
component_hash: 3e8f7d34e04bfa6333f289a6d126745bebe0cee9311b81a6a5d106047f63ef65
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com
type: service
version: 0.7.0
espressif/esp-dl:
component_hash: 233ee8d37dfedeb637fc74713b87bd861ac530cfbcc4c63c57c2e51064cdda31
dependencies:
- name: espressif/dl_fft
registry_url: https://components.espressif.com
require: private
version: '>=0.6.0'
- name: espressif/esp_new_jpeg
registry_url: https://components.espressif.com
require: private
version: ^1
- name: idf
require: private
version: '*'
source:
registry_url: https://components.espressif.com
type: service
targets:
- esp32
- esp32c2
- esp32c3
- esp32c5
- esp32c6
- esp32p4
- esp32s2
- esp32s3
- esp32s31
version: 3.3.11
espressif/esp32-camera:
component_hash: bc9c8a6b51df777a014fa295825b3de5069bc0300c317acff20c97cf4a10ac7d
dependencies:
- name: espressif/esp_jpeg
registry_url: https://components.espressif.com
require: public
version: ^1.3.1
- name: idf
require: private
version: '>=5.1'
source:
registry_url: https://components.espressif.com/
type: service
version: 2.1.7
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com
type: service
version: 1.3.1
espressif/esp_new_jpeg:
component_hash: e1e7e6991193c41ed6c9fc386702aa7821433319e7aa9c3147cd3236310cd69c
dependencies: []
source:
registry_url: https://components.espressif.com
type: service
targets:
- esp32
- esp32s2
- esp32s3
- esp32s31
- esp32p4
- esp32c2
- esp32c3
- esp32c5
- esp32c6
- esp32c61
- esp32h4
version: 1.0.2
espressif/pedestrian_detect:
component_hash: 882a6986cb5e7e9bb8e87861fb42379a7eeb2f4a4a78104c6a78f42a6cb35fb4
dependencies:
- name: espressif/esp-dl
registry_url: https://components.espressif.com
require: private
version: ~3.3.0
source:
registry_url: https://components.espressif.com/
type: service
version: 0.3.2
idf:
source:
type: idf
version: 5.5.2
direct_dependencies:
- espressif/esp32-camera
- espressif/pedestrian_detect
manifest_hash: da1a3cbbaa4755170f4ffdc5b90495e507c88e07fa97fb46cd1af3e5f728a198
target: esp32s3
version: 2.0.0
@@ -0,0 +1,114 @@
Executing action: flash
Running ninja in directory /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build
Executing "ninja flash"...
[1/5] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/partition_table/partition-table.bin /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
freddy_camera_person.bin binary size 0x288bd0 bytes. Smallest app partition is 0xa00000 bytes. 0x777430 bytes (75%) free.
[2/5] Performing build step for 'bootloader'
[1/1] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/bootloader.bin
Bootloader binary size 0x5240 bytes. 0x2dc0 bytes (36%) free.
[3/5] No install step for 'bootloader'
[4/5] Completed 'bootloader'
[4/5] cd /Users/joshua/esp/esp-idf/components/esptool_py && /opt/homebrew/bin/cmake -D IDF_PATH=/Users/joshua/esp/esp-idf -D "SERIAL_TOOL=/Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python;;/Users/joshua/esp/esp-idf/components/esptool_py/esptool/esptool.py;--chip;esp32s3" -D "SERIAL_TOOL_ARGS=--before=default_reset;--after=hard_reset;write_flash;@flash_args" -D WORKING_DIRECTORY=/Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build -P /Users/joshua/esp/esp-idf/components/esptool_py/run_serial_tool.cmake
esptool.py --chip esp32s3 -p /dev/cu.usbmodem1101 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 16MB 0x0 bootloader/bootloader.bin 0x10000 freddy_camera_person.bin 0x8000 partition_table/partition-table.bin
esptool.py v4.12.dev1
Serial port /dev/cu.usbmodem1101
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: e8:f6:0a:87:0b:4c
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x00298fff...
Flash will be erased from 0x00008000 to 0x00008fff...
SHA digest in image updated
Compressed 21056 bytes to 13390...
Writing at 0x00000000... (100 %)
Wrote 21056 bytes (13390 compressed) at 0x00000000 in 0.4 seconds (effective 405.0 kbit/s)...
Hash of data verified.
Compressed 2657232 bytes to 1115837...
Writing at 0x00010000... (1 %)
Writing at 0x0001e8c5... (2 %)
Writing at 0x00026ccf... (4 %)
Writing at 0x0002cb86... (5 %)
Writing at 0x0003235f... (7 %)
Writing at 0x00038125... (8 %)
Writing at 0x0003de36... (10 %)
Writing at 0x00043c8c... (11 %)
Writing at 0x000496a3... (13 %)
Writing at 0x0004fa83... (14 %)
Writing at 0x000557c4... (15 %)
Writing at 0x0005ae4b... (17 %)
Writing at 0x000605c4... (18 %)
Writing at 0x00066665... (20 %)
Writing at 0x0006bbdb... (21 %)
Writing at 0x0007236c... (23 %)
Writing at 0x0007744c... (24 %)
Writing at 0x0007d49f... (26 %)
Writing at 0x00082cef... (27 %)
Writing at 0x0009577a... (28 %)
Writing at 0x0009c3a6... (30 %)
Writing at 0x000a3a13... (31 %)
Writing at 0x000aa016... (33 %)
Writing at 0x000b15d1... (34 %)
Writing at 0x000b896d... (36 %)
Writing at 0x000bfd09... (37 %)
Writing at 0x000c8751... (39 %)
Writing at 0x000ce815... (40 %)
Writing at 0x000d4c5a... (42 %)
Writing at 0x000dbeea... (43 %)
Writing at 0x000e55f3... (44 %)
Writing at 0x000edb25... (46 %)
Writing at 0x000f3d22... (47 %)
Writing at 0x000fad22... (49 %)
Writing at 0x00102d30... (50 %)
Writing at 0x0010cd0e... (52 %)
Writing at 0x001156a3... (53 %)
Writing at 0x0011c149... (55 %)
Writing at 0x00123b6e... (56 %)
Writing at 0x0012d6a3... (57 %)
Writing at 0x00135e65... (59 %)
Writing at 0x0013e021... (60 %)
Writing at 0x00148b3b... (62 %)
Writing at 0x001566b5... (63 %)
Writing at 0x0015fa75... (65 %)
Writing at 0x00168e33... (66 %)
Writing at 0x00172f27... (68 %)
Writing at 0x0017c88d... (69 %)
Writing at 0x00186d9a... (71 %)
Writing at 0x0019200d... (72 %)
Writing at 0x0019e78a... (73 %)
Writing at 0x001a8ad4... (75 %)
Writing at 0x001b3649... (76 %)
Writing at 0x001b98b0... (78 %)
Writing at 0x001c74e4... (79 %)
Writing at 0x001d3605... (81 %)
Writing at 0x001dcaa2... (82 %)
Writing at 0x001e800e... (84 %)
Writing at 0x001f21ae... (85 %)
Writing at 0x00211ab2... (86 %)
Writing at 0x0021a5bf... (88 %)
Writing at 0x00222864... (89 %)
Writing at 0x0022c094... (91 %)
Writing at 0x0023283c... (92 %)
Writing at 0x0023dd77... (94 %)
Writing at 0x0027ca68... (95 %)
Writing at 0x0028a884... (97 %)
Writing at 0x00291532... (98 %)
Writing at 0x00297f10... (100 %)
Wrote 2657232 bytes (1115837 compressed) at 0x00010000 in 19.5 seconds (effective 1088.0 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 105...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (105 compressed) at 0x00008000 in 0.1 seconds (effective 315.7 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Done
@@ -0,0 +1,114 @@
Executing action: flash
Running ninja in directory /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build
Executing "ninja flash"...
[1/5] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/partition_table/partition-table.bin /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/freddy_camera_person.bin
freddy_camera_person.bin binary size 0x288e00 bytes. Smallest app partition is 0xa00000 bytes. 0x777200 bytes (75%) free.
[2/5] Performing build step for 'bootloader'
[1/1] cd /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/esp-idf/esptool_py && /Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python /Users/joshua/esp/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build/bootloader/bootloader.bin
Bootloader binary size 0x5240 bytes. 0x2dc0 bytes (36%) free.
[3/5] No install step for 'bootloader'
[4/5] Completed 'bootloader'
[4/5] cd /Users/joshua/esp/esp-idf/components/esptool_py && /opt/homebrew/bin/cmake -D IDF_PATH=/Users/joshua/esp/esp-idf -D "SERIAL_TOOL=/Users/joshua/.espressif/python_env/idf5.5_py3.13_env/bin/python;;/Users/joshua/esp/esp-idf/components/esptool_py/esptool/esptool.py;--chip;esp32s3" -D "SERIAL_TOOL_ARGS=--before=default_reset;--after=hard_reset;write_flash;@flash_args" -D WORKING_DIRECTORY=/Users/joshua/Code/3d-creations/freddy_fazbear_digital_eyes/firmware/camera_person_idf/build -P /Users/joshua/esp/esp-idf/components/esptool_py/run_serial_tool.cmake
esptool.py --chip esp32s3 -p /dev/cu.usbmodem1101 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 16MB 0x0 bootloader/bootloader.bin 0x10000 freddy_camera_person.bin 0x8000 partition_table/partition-table.bin
esptool.py v4.12.dev1
Serial port /dev/cu.usbmodem1101
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: e8:f6:0a:87:0b:4c
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x00298fff...
Flash will be erased from 0x00008000 to 0x00008fff...
SHA digest in image updated
Compressed 21056 bytes to 13390...
Writing at 0x00000000... (100 %)
Wrote 21056 bytes (13390 compressed) at 0x00000000 in 0.4 seconds (effective 404.8 kbit/s)...
Hash of data verified.
Compressed 2657792 bytes to 1116115...
Writing at 0x00010000... (1 %)
Writing at 0x0001e8b0... (2 %)
Writing at 0x00026d2d... (4 %)
Writing at 0x0002cbde... (5 %)
Writing at 0x000323bd... (7 %)
Writing at 0x0003817d... (8 %)
Writing at 0x0003de91... (10 %)
Writing at 0x00043ce2... (11 %)
Writing at 0x000496ef... (13 %)
Writing at 0x0004fad8... (14 %)
Writing at 0x0005582a... (15 %)
Writing at 0x0005aeab... (17 %)
Writing at 0x0006062a... (18 %)
Writing at 0x000666c3... (20 %)
Writing at 0x0006bc3f... (21 %)
Writing at 0x000723ba... (23 %)
Writing at 0x000774ab... (24 %)
Writing at 0x0007d509... (26 %)
Writing at 0x00082d47... (27 %)
Writing at 0x00095771... (28 %)
Writing at 0x0009c3f7... (30 %)
Writing at 0x000a39a3... (31 %)
Writing at 0x000a9f90... (33 %)
Writing at 0x000b150c... (34 %)
Writing at 0x000b88cb... (36 %)
Writing at 0x000bfc9f... (37 %)
Writing at 0x000c86ac... (39 %)
Writing at 0x000ce799... (40 %)
Writing at 0x000d4bff... (42 %)
Writing at 0x000dbd35... (43 %)
Writing at 0x000e55ae... (44 %)
Writing at 0x000edaec... (46 %)
Writing at 0x000f3cc1... (47 %)
Writing at 0x000facde... (49 %)
Writing at 0x00102cc4... (50 %)
Writing at 0x0010ccc5... (52 %)
Writing at 0x00115672... (53 %)
Writing at 0x0011c0eb... (55 %)
Writing at 0x00123ad4... (56 %)
Writing at 0x0012d46e... (57 %)
Writing at 0x00135daf... (59 %)
Writing at 0x0013df8a... (60 %)
Writing at 0x00148995... (62 %)
Writing at 0x001565f9... (63 %)
Writing at 0x0015f9a4... (65 %)
Writing at 0x00168bea... (66 %)
Writing at 0x00172e2d... (68 %)
Writing at 0x0017c7f4... (69 %)
Writing at 0x00186d0a... (71 %)
Writing at 0x00191e8e... (72 %)
Writing at 0x0019e70d... (73 %)
Writing at 0x001a8a1b... (75 %)
Writing at 0x001b354d... (76 %)
Writing at 0x001b9818... (78 %)
Writing at 0x001c73a9... (79 %)
Writing at 0x001d3588... (81 %)
Writing at 0x001dc97d... (82 %)
Writing at 0x001e7f86... (84 %)
Writing at 0x001f2161... (85 %)
Writing at 0x00211397... (86 %)
Writing at 0x0021a4e4... (88 %)
Writing at 0x00222832... (89 %)
Writing at 0x0022bf16... (91 %)
Writing at 0x0023281e... (92 %)
Writing at 0x0023d6d4... (94 %)
Writing at 0x00278406... (95 %)
Writing at 0x0028a879... (97 %)
Writing at 0x0029159d... (98 %)
Writing at 0x00297f4b... (100 %)
Wrote 2657792 bytes (1116115 compressed) at 0x00010000 in 19.5 seconds (effective 1092.8 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 105...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (105 compressed) at 0x00008000 in 0.1 seconds (effective 352.1 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Done
@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.cpp" "ota_service.cpp" INCLUDE_DIRS "."
REQUIRES app_update esp_http_server esp_wifi esp_netif
nvs_flash mbedtls)
@@ -0,0 +1,3 @@
dependencies:
espressif/esp32-camera: "^2.1.7"
espressif/pedestrian_detect: "^0.3.2"
+251
View File
@@ -0,0 +1,251 @@
// DFR1154 V1.1 person-position experiment using Espressif's pedestrian model.
// Camera pin assignments match DFRobot's DFR1154 CameraWebServer example.
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include "dl_image_jpeg.hpp"
#include "driver/gpio.h"
#include "esp_camera.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "pedestrian_detect.hpp"
#include "driver/uart.h"
#include "../../shared/GazeProtocol.h"
#include "../../shared/TrackingGate.h"
#include "ota_service.h"
namespace {
constexpr char kTag[] = "freddy_person";
constexpr float kDiagnosticScoreThreshold = 0.35f;
constexpr gpio_num_t kIrLedPin = GPIO_NUM_47;
constexpr uart_port_t kGazeUart = UART_NUM_1;
constexpr int kGazeTxPin = 43;
constexpr int kGazeBaud = 115200;
void init_gaze_uart() {
const uart_config_t config = {
.baud_rate = kGazeBaud,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0,
.source_clk = UART_SCLK_DEFAULT,
.flags = {},
};
ESP_ERROR_CHECK(uart_driver_install(kGazeUart, 1024, 0, 0, nullptr, 0));
ESP_ERROR_CHECK(uart_param_config(kGazeUart, &config));
ESP_ERROR_CHECK(uart_set_pin(kGazeUart, kGazeTxPin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_LOGI(kTag, "GAZE_UART_READY tx_gpio=%d baud=%d", kGazeTxPin, kGazeBaud);
}
void send_gaze(uint16_t sequence, int x, int y, bool valid) {
gaze::Frame command = {sequence, static_cast<uint16_t>(x),
static_cast<uint16_t>(y), valid};
uint8_t bytes[gaze::kFrameSize];
gaze::encode(command, bytes);
if (uart_write_bytes(kGazeUart, bytes, sizeof(bytes)) != sizeof(bytes)) {
ESP_LOGW(kTag, "GAZE_UART_WRITE_ERROR seq=%u", sequence);
}
}
#ifndef FREDDY_GAZE_LINK_SELF_TEST
#define FREDDY_GAZE_LINK_SELF_TEST 0
#endif
#if FREDDY_GAZE_LINK_SELF_TEST
// Bench-only UART fault injection. Keep disabled in the normal camera build.
void run_gaze_link_self_test() {
uint8_t bytes[gaze::kFrameSize];
gaze::encode({0xF000, 500, 500, false}, bytes);
uart_write_bytes(kGazeUart, bytes, sizeof(bytes));
vTaskDelay(pdMS_TO_TICKS(150));
uart_write_bytes(kGazeUart, bytes, sizeof(bytes)); // Duplicate sequence.
vTaskDelay(pdMS_TO_TICKS(150));
gaze::encode({0xEFFF, 500, 500, false}, bytes);
uart_write_bytes(kGazeUart, bytes, sizeof(bytes)); // Old sequence.
vTaskDelay(pdMS_TO_TICKS(150));
gaze::encode({0xF001, 500, 500, false}, bytes);
bytes[2] = 2; // Wrong version.
uart_write_bytes(kGazeUart, bytes, sizeof(bytes));
vTaskDelay(pdMS_TO_TICKS(150));
gaze::encode({0xF001, 500, 500, false}, bytes);
bytes[10] ^= 1; // Damaged CRC.
uart_write_bytes(kGazeUart, bytes, sizeof(bytes));
vTaskDelay(pdMS_TO_TICKS(150));
send_gaze(0xF002, 500, 500, false); // Recovery.
ESP_LOGI(kTag, "GAZE_LINK_SELF_TEST_SENT");
}
#endif
#ifndef FREDDY_CAMERA_DIAGNOSTIC_JPEG
#define FREDDY_CAMERA_DIAGNOSTIC_JPEG 0
#endif
#if FREDDY_CAMERA_DIAGNOSTIC_JPEG
// Optional bench diagnostic: one JPEG roughly every five seconds.
void print_jpeg_for_diagnostics(const camera_fb_t *frame, unsigned frame_number) {
printf("FRAME_BEGIN %u %u\n", frame_number, static_cast<unsigned>(frame->len));
for (size_t offset = 0; offset < frame->len; offset += 48) {
printf("FRAME_HEX ");
for (size_t i = offset; i < frame->len && i < offset + 48; ++i) {
printf("%02x", frame->buf[i]);
}
printf("\n");
}
printf("FRAME_END %u\n", frame_number);
fflush(stdout);
}
#endif
camera_config_t camera_config() {
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;
return config;
}
} // namespace
extern "C" void app_main(void) {
init_gaze_uart();
#if FREDDY_GAZE_LINK_SELF_TEST
run_gaze_link_self_test();
#endif
ESP_ERROR_CHECK(gpio_reset_pin(kIrLedPin));
ESP_ERROR_CHECK(gpio_set_direction(kIrLedPin, GPIO_MODE_OUTPUT));
ESP_ERROR_CHECK(gpio_set_level(kIrLedPin, 1));
ESP_LOGI(kTag, "IR_GPIO47_HIGH");
camera_config_t config = camera_config();
const esp_err_t camera_error = esp_camera_init(&config);
if (camera_error != ESP_OK) {
ESP_LOGE(kTag, "CAMERA_INIT_ERROR 0x%X", camera_error);
return;
}
sensor_t *sensor = esp_camera_sensor_get();
if (sensor) {
ESP_LOGI(kTag, "SENSOR_PID 0x%04X", sensor->id.PID);
if (sensor->id.PID == OV3660_PID) sensor->set_vflip(sensor, 1);
}
auto *detector = new PedestrianDetect();
detector->set_score_thr(kDiagnosticScoreThreshold);
ESP_LOGI(kTag, "PERSON_DETECTOR_READY threshold=%.2f", kDiagnosticScoreThreshold);
ota_service_start();
ota_service_confirm_healthy();
unsigned frame_number = 0;
uint16_t gaze_sequence = 0;
tracking::Gate target_gate;
for (;;) {
const int64_t capture_started = esp_timer_get_time();
camera_fb_t *frame = esp_camera_fb_get();
if (!frame) {
ESP_LOGW(kTag, "CAPTURE_ERROR");
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
++frame_number;
#if FREDDY_CAMERA_DIAGNOSTIC_JPEG
if (frame_number % 20 == 1) {
print_jpeg_for_diagnostics(frame, frame_number);
}
#endif
dl::image::jpeg_img_t jpeg = {
.data = frame->buf,
.data_len = frame->len,
};
auto image = dl::image::sw_decode_jpeg(jpeg, dl::image::DL_IMAGE_PIX_TYPE_RGB888);
esp_camera_fb_return(frame);
if (!image.data) {
ESP_LOGW(kTag, "DECODE_ERROR");
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
auto &results = detector->run(image);
const int64_t completed = esp_timer_get_time();
const dl::detect::result_t *best = nullptr;
int rejected_full_frame = 0;
int rejected_full_height = 0;
for (const auto &result : results) {
const int box_width = result.box[2] - result.box[0];
const int box_height = result.box[3] - result.box[1];
// The empty-scene diagnostic repeatedly labels almost the entire image
// as a person. Keep this shape from commanding either eye.
if (box_width >= image.width * 9 / 10 &&
box_height >= image.height * 9 / 10) {
++rejected_full_frame;
continue;
}
// The confirmed empty view repeatedly produces boxes running nearly
// from the top edge to the bottom edge. Earlier real-person bench boxes
// did not have this geometry; validate again at the installed distance.
if (box_height >= image.height * 95 / 100 && result.box[1] <= 2) {
++rejected_full_height;
continue;
}
if (!best || result.score > best->score) best = &result;
}
const int center_x = best ? (best->box[0] + best->box[2]) / 2 : 0;
const int center_y = best ? (best->box[1] + best->box[3]) / 2 : 0;
const tracking::Decision target = target_gate.update(
best != nullptr, center_x, center_y,
static_cast<uint32_t>(completed / 1000));
const int gaze_x = (!best && !target.valid)
? 500
: std::clamp(target.x, 0, image.width - 1) * 1000 /
(image.width - 1);
const int gaze_y = (!best && !target.valid)
? 500
: std::clamp(target.y, 0, image.height - 1) * 1000 /
(image.height - 1);
send_gaze(gaze_sequence++, gaze_x, gaze_y, target.valid);
if (best) {
ESP_LOGI(kTag,
"PERSON t_ms=%lld score=%.3f cx=%d cy=%d box=%d,%d,%d,%d frame=%d,%d latency_ms=%lld gaze_valid=%d target_hold=%d rejected_full_height=%d",
static_cast<long long>(completed / 1000), best->score, center_x,
center_y, best->box[0], best->box[1], best->box[2], best->box[3],
image.width, image.height,
static_cast<long long>((completed - capture_started) / 1000),
target.valid, target.held, rejected_full_height);
} else {
ESP_LOGI(kTag, "NO_PERSON t_ms=%lld frame=%d,%d latency_ms=%lld rejected_full_frame=%d rejected_full_height=%d gaze_valid=%d target_hold=%d",
static_cast<long long>(completed / 1000), image.width, image.height,
static_cast<long long>((completed - capture_started) / 1000),
rejected_full_frame, rejected_full_height, target.valid,
target.held);
}
heap_caps_free(image.data);
vTaskDelay(pdMS_TO_TICKS(20));
}
}
@@ -0,0 +1,195 @@
#include "ota_service.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_ota_ops.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mbedtls/sha256.h"
#include "nvs_flash.h"
#include "../../shared/OtaAuth.h"
#include "../../shared/OtaSecrets.h"
namespace {
constexpr char kTag[] = "freddy_ota";
constexpr char kDevice[] = "camera";
constexpr char kImageTarget[] = "FREDDY_OTA_TARGET:camera";
constexpr int kPort = 3232;
char nonce[33] = {};
int64_t nonce_time_us = 0;
httpd_handle_t server = nullptr;
void new_nonce() {
uint8_t random_bytes[16];
esp_fill_random(random_bytes, sizeof(random_bytes));
freddy_ota::to_hex(random_bytes, sizeof(random_bytes), nonce);
nonce_time_us = esp_timer_get_time();
}
esp_err_t info_handler(httpd_req_t *req) {
new_nonce();
const esp_partition_t *slot = esp_ota_get_next_update_partition(nullptr);
const esp_partition_t *running = esp_ota_get_running_partition();
char body[180];
snprintf(body, sizeof(body),
"{\"device\":\"%s\",\"nonce\":\"%s\",\"slot_bytes\":%u,\"running_slot\":%d}",
kDevice, nonce, slot ? static_cast<unsigned>(slot->size) : 0,
running ? static_cast<int>(running->subtype) - ESP_PARTITION_SUBTYPE_APP_OTA_0 : -1);
httpd_resp_set_type(req, "application/json");
return httpd_resp_sendstr(req, body);
}
bool read_header(httpd_req_t *req, const char *name, char *out, size_t capacity) {
const size_t length = httpd_req_get_hdr_value_len(req, name);
return length > 0 && length < capacity &&
httpd_req_get_hdr_value_str(req, name, out, capacity) == ESP_OK;
}
void restart_task(void *) {
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
}
esp_err_t upload_handler(httpd_req_t *req) {
char request_nonce[33], request_sha[65], signature[65];
const esp_partition_t *slot = esp_ota_get_next_update_partition(nullptr);
if (!slot || req->content_len <= 0 ||
static_cast<size_t>(req->content_len) > slot->size ||
!read_header(req, "X-Freddy-Nonce", request_nonce, sizeof(request_nonce)) ||
!read_header(req, "X-Freddy-SHA256", request_sha, sizeof(request_sha)) ||
!read_header(req, "X-Freddy-Signature", signature, sizeof(signature)) ||
esp_timer_get_time() - nonce_time_us > 60000000 ||
strcmp(request_nonce, nonce) != 0 ||
!freddy_ota::verify(FREDDY_OTA_KEY, kDevice, nonce, request_sha,
req->content_len, signature)) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "OTA authorization failed");
return ESP_FAIL;
}
nonce[0] = 0; // Prevent replay even after an interrupted upload.
esp_ota_handle_t handle;
esp_err_t result = esp_ota_begin(slot, req->content_len, &handle);
if (result != ESP_OK) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
return ESP_FAIL;
}
mbedtls_sha256_context sha_context;
mbedtls_sha256_init(&sha_context);
result = mbedtls_sha256_starts(&sha_context, 0) == 0 ? ESP_OK : ESP_FAIL;
uint8_t buffer[2048];
int remaining = req->content_len;
unsigned timeouts = 0;
while (result == ESP_OK && remaining > 0) {
const int received = httpd_req_recv(
req, reinterpret_cast<char *>(buffer),
remaining < static_cast<int>(sizeof(buffer)) ? remaining : sizeof(buffer));
if (received == HTTPD_SOCK_ERR_TIMEOUT && ++timeouts < 5) continue;
if (received <= 0) {
result = ESP_FAIL;
break;
}
timeouts = 0;
result = esp_ota_write(handle, buffer, received);
if (result == ESP_OK && mbedtls_sha256_update(&sha_context, buffer, received) != 0)
result = ESP_FAIL;
remaining -= received;
}
uint8_t digest[32] = {};
if (result == ESP_OK &&
mbedtls_sha256_finish(&sha_context, digest) != 0) result = ESP_FAIL;
mbedtls_sha256_free(&sha_context);
char actual_sha[65];
freddy_ota::to_hex(digest, sizeof(digest), actual_sha);
if (result == ESP_OK &&
!freddy_ota::constant_time_equal(actual_sha, request_sha, 64))
result = ESP_FAIL;
if (result == ESP_OK) result = esp_ota_end(handle);
else esp_ota_abort(handle);
if (result == ESP_OK) result = esp_ota_set_boot_partition(slot);
if (result != ESP_OK) {
ESP_LOGE(kTag, "OTA upload failed: %s", esp_err_to_name(result));
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA image rejected");
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"ok\":true}");
xTaskCreate(restart_task, "ota_restart", 2048, nullptr, 5, nullptr);
return ESP_OK;
}
void wifi_event(void *, esp_event_base_t base, int32_t id, void *data) {
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
const auto *event = static_cast<ip_event_got_ip_t *>(data);
ESP_LOGI(kTag, "OTA_READY device=%s image=%s ip=" IPSTR " port=%d",
kDevice, kImageTarget, IP2STR(&event->ip_info.ip), kPort);
}
}
} // namespace
void ota_service_start() {
if (!FREDDY_WIFI_SSID[0] || strlen(FREDDY_OTA_KEY) < 32) {
ESP_LOGW(kTag, "OTA_DISABLED: configure firmware/private/ first");
return;
}
esp_err_t result = nvs_flash_init();
if (result == ESP_ERR_NVS_NO_FREE_PAGES ||
result == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
result = nvs_flash_init();
}
ESP_ERROR_CHECK(result);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t init = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&init));
ESP_ERROR_CHECK(esp_event_handler_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event, nullptr));
ESP_ERROR_CHECK(esp_event_handler_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event, nullptr));
wifi_config_t config = {};
if (strlen(FREDDY_WIFI_SSID) >= sizeof(config.sta.ssid) ||
strlen(FREDDY_WIFI_PASSWORD) >= sizeof(config.sta.password)) {
ESP_LOGE(kTag, "Wi-Fi credentials exceed ESP32 limits");
return;
}
strncpy(reinterpret_cast<char *>(config.sta.ssid), FREDDY_WIFI_SSID,
sizeof(config.sta.ssid) - 1);
strncpy(reinterpret_cast<char *>(config.sta.password), FREDDY_WIFI_PASSWORD,
sizeof(config.sta.password) - 1);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_connect());
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
http_config.server_port = kPort;
http_config.stack_size = 8192;
http_config.lru_purge_enable = true;
ESP_ERROR_CHECK(httpd_start(&server, &http_config));
const httpd_uri_t info = {
.uri = "/ota/info", .method = HTTP_GET, .handler = info_handler,
.user_ctx = nullptr,
};
const httpd_uri_t upload = {
.uri = "/ota", .method = HTTP_POST, .handler = upload_handler,
.user_ctx = nullptr,
};
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &info));
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &upload));
}
void ota_service_confirm_healthy() {
const esp_err_t result = esp_ota_mark_app_valid_cancel_rollback();
if (result == ESP_OK) ESP_LOGI(kTag, "OTA_IMAGE_CONFIRMED");
}
@@ -0,0 +1,4 @@
#pragma once
void ota_service_start();
void ota_service_confirm_healthy();
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x4000
otadata, data, ota, 0xd000, 0x2000
phy_init, data, phy, 0xf000, 0x1000
ota_0, app, ota_0, 0x10000, 0x700000
ota_1, app, ota_1, 0x710000, 0x700000
1 # Name Type SubType Offset Size
2 nvs data nvs 0x9000 0x4000
3 otadata data ota 0xd000 0x2000
4 phy_init data phy 0xf000 0x1000
5 ota_0 app ota_0 0x10000 0x700000
6 ota_1 app ota_1 0x710000 0x700000
@@ -0,0 +1,11 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
+16
View File
@@ -0,0 +1,16 @@
#include "BAT_Driver.h"
float BAT_analogVolts = 0;
void BAT_Init(void)
{
//set the resolution to 12 bits (0-4095)
analogReadResolution(12);
}
float BAT_Get_Volts(void)
{
int Volts = analogReadMilliVolts(BAT_ADC_PIN); // millivolts
BAT_analogVolts = (float)(Volts * 3.0 / 1000.0) / Measurement_offset;
// printf("BAT voltage : %.2f V\r\n", BAT_analogVolts);
return BAT_analogVolts;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <Arduino.h>
#define BAT_ADC_PIN 4
#define Measurement_offset 0.992857
extern float BAT_analogVolts;
void BAT_Init(void);
float BAT_Get_Volts(void);
+428
View File
@@ -0,0 +1,428 @@
#include "Display_ST7701.h"
spi_device_handle_t SPI_handle = NULL;
esp_lcd_panel_handle_t panel_handle = NULL;
void ST7701_WriteCommand(uint8_t cmd)
{
spi_transaction_t spi_tran = {
.cmd = 0,
.addr = cmd,
.length = 0,
.rxlength = 0,
};
spi_device_transmit(SPI_handle, &spi_tran);
}
void ST7701_WriteData(uint8_t data)
{
spi_transaction_t spi_tran = {
.cmd = 1,
.addr = data,
.length = 0,
.rxlength = 0,
};
spi_device_transmit(SPI_handle, &spi_tran);
}
void ST7701_CS_EN(){
Set_EXIO(EXIO_PIN3,Low);
vTaskDelay(pdMS_TO_TICKS(10));
}
void ST7701_CS_Dis(){
Set_EXIO(EXIO_PIN3,High);
vTaskDelay(pdMS_TO_TICKS(10));
}
void ST7701_Reset(){
Set_EXIO(EXIO_PIN1,Low);
vTaskDelay(pdMS_TO_TICKS(10));
Set_EXIO(EXIO_PIN1,High);
vTaskDelay(pdMS_TO_TICKS(50));
}
void ST7701_Init()
{
spi_bus_config_t buscfg = {
.mosi_io_num = LCD_MOSI_PIN,
.miso_io_num = -1,
.sclk_io_num = LCD_CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 64, // ESP32 S3 max size is 64Kbytes
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 8,
.mode = SPI_MODE0,
.clock_speed_hz = 40000000,
.spics_io_num = -1,
.queue_size = 1, // Not using queues
};
spi_bus_add_device(SPI2_HOST, &devcfg, &SPI_handle);
ST7701_CS_EN();
ST7701_WriteCommand(0xFF);
ST7701_WriteData(0x77);
ST7701_WriteData(0x01);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x10);
ST7701_WriteCommand(0xC0);
ST7701_WriteData(0x3B);//Scan line
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xC1);
ST7701_WriteData(0x0B); //VBP
ST7701_WriteData(0x02);
ST7701_WriteCommand(0xC2);
ST7701_WriteData(0x07);
ST7701_WriteData(0x02);
ST7701_WriteCommand(0xCC);
ST7701_WriteData(0x10);
ST7701_WriteCommand(0xCD);//RGB format
ST7701_WriteData(0x08);
ST7701_WriteCommand(0xB0); // IPS
ST7701_WriteData(0x00); // 255
ST7701_WriteData(0x11); // 251
ST7701_WriteData(0x16); // 247 down
ST7701_WriteData(0x0e); // 239
ST7701_WriteData(0x11); // 231
ST7701_WriteData(0x06); // 203
ST7701_WriteData(0x05); // 175
ST7701_WriteData(0x09); // 147
ST7701_WriteData(0x08); // 108
ST7701_WriteData(0x21); // 80
ST7701_WriteData(0x06); // 52
ST7701_WriteData(0x13); // 24
ST7701_WriteData(0x10); // 16
ST7701_WriteData(0x29); // 8 down
ST7701_WriteData(0x31); // 4
ST7701_WriteData(0x18); // 0
ST7701_WriteCommand(0xB1);// IPS
ST7701_WriteData(0x00);// 255
ST7701_WriteData(0x11);// 251
ST7701_WriteData(0x16);// 247 down
ST7701_WriteData(0x0e);// 239
ST7701_WriteData(0x11);// 231
ST7701_WriteData(0x07);// 203
ST7701_WriteData(0x05);// 175
ST7701_WriteData(0x09);// 147
ST7701_WriteData(0x09);// 108
ST7701_WriteData(0x21);// 80
ST7701_WriteData(0x05);// 52
ST7701_WriteData(0x13);// 24
ST7701_WriteData(0x11);// 16
ST7701_WriteData(0x2a);// 8 down
ST7701_WriteData(0x31);// 4
ST7701_WriteData(0x18);// 0
ST7701_WriteCommand(0xFF);
ST7701_WriteData(0x77);
ST7701_WriteData(0x01);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x11);
ST7701_WriteCommand(0xB0); //VOP 3.5375+ *x 0.0125
ST7701_WriteData(0x6d); //5D
ST7701_WriteCommand(0xB1); //VCOM amplitude setting
ST7701_WriteData(0x37); //
ST7701_WriteCommand(0xB2); //VGH Voltage setting
ST7701_WriteData(0x81); //12V
ST7701_WriteCommand(0xB3);
ST7701_WriteData(0x80);
ST7701_WriteCommand(0xB5); //VGL Voltage setting
ST7701_WriteData(0x43); //-8.3V
ST7701_WriteCommand(0xB7);
ST7701_WriteData(0x85);
ST7701_WriteCommand(0xB8);
ST7701_WriteData(0x20);
ST7701_WriteCommand(0xC1);
ST7701_WriteData(0x78);
ST7701_WriteCommand(0xC2);
ST7701_WriteData(0x78);
ST7701_WriteCommand(0xD0);
ST7701_WriteData(0x88);
ST7701_WriteCommand(0xE0);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x02);
ST7701_WriteCommand(0xE1);
ST7701_WriteData(0x03);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x04);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x20);
ST7701_WriteData(0x20);
ST7701_WriteCommand(0xE2);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE3);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x11);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE4);
ST7701_WriteData(0x22);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE5);
ST7701_WriteData(0x05);
ST7701_WriteData(0xEC);
ST7701_WriteData(0xA0);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x07);
ST7701_WriteData(0xEE);
ST7701_WriteData(0xA0);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE6);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x11);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE7);
ST7701_WriteData(0x22);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xE8);
ST7701_WriteData(0x06);
ST7701_WriteData(0xED);
ST7701_WriteData(0xA0);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x08);
ST7701_WriteData(0xEF);
ST7701_WriteData(0xA0);
ST7701_WriteData(0xA0);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xEB);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x40);
ST7701_WriteData(0x40);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0xED);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xBA);
ST7701_WriteData(0x0A);
ST7701_WriteData(0xBF);
ST7701_WriteData(0x45);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xFF);
ST7701_WriteData(0x54);
ST7701_WriteData(0xFB);
ST7701_WriteData(0xA0);
ST7701_WriteData(0xAB);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xFF);
ST7701_WriteData(0xFF);
ST7701_WriteCommand(0xEF);
ST7701_WriteData(0x10);
ST7701_WriteData(0x0D);
ST7701_WriteData(0x04);
ST7701_WriteData(0x08);
ST7701_WriteData(0x3F);
ST7701_WriteData(0x1F);
ST7701_WriteCommand(0xFF);
ST7701_WriteData(0x77);
ST7701_WriteData(0x01);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x13);
ST7701_WriteCommand(0xEF);
ST7701_WriteData(0x08);
ST7701_WriteCommand(0xFF);
ST7701_WriteData(0x77);
ST7701_WriteData(0x01);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0x36);
ST7701_WriteData(0x00);
ST7701_WriteCommand(0x3A);
ST7701_WriteData(0x66);
ST7701_WriteCommand(0x11);
vTaskDelay(pdMS_TO_TICKS(480));
ST7701_WriteCommand(0x20); //
vTaskDelay(pdMS_TO_TICKS(120));
ST7701_WriteCommand(0x29);
ST7701_CS_Dis();
// RGB
esp_lcd_rgb_panel_config_t rgb_config = {
.clk_src = LCD_CLK_SRC_DEFAULT,
.timings = {
.pclk_hz = ESP_PANEL_LCD_RGB_TIMING_FREQ_HZ,
.h_res = ESP_PANEL_LCD_HEIGHT,
.v_res = ESP_PANEL_LCD_WIDTH,
.hsync_pulse_width = ESP_PANEL_LCD_RGB_TIMING_HPW,
.hsync_back_porch = ESP_PANEL_LCD_RGB_TIMING_HBP,
.hsync_front_porch = ESP_PANEL_LCD_RGB_TIMING_HFP,
.vsync_pulse_width = ESP_PANEL_LCD_RGB_TIMING_VPW,
.vsync_back_porch = ESP_PANEL_LCD_RGB_TIMING_VBP,
.vsync_front_porch = ESP_PANEL_LCD_RGB_TIMING_VFP,
.flags = {
.hsync_idle_low = 0, /*!< The hsync signal is low in IDLE state */
.vsync_idle_low = 0, /*!< The vsync signal is low in IDLE state */
.de_idle_high = 0, /*!< The de signal is high in IDLE state */
.pclk_active_neg = false,
.pclk_idle_high = 0, /*!< The PCLK stays at high level in IDLE phase */
},
},
.data_width = ESP_PANEL_LCD_RGB_DATA_WIDTH,
.bits_per_pixel = ESP_PANEL_LCD_RGB_PIXEL_BITS,
.num_fbs = ESP_PANEL_LCD_RGB_FRAME_BUF_NUM,
.bounce_buffer_size_px = 10 * ESP_PANEL_LCD_HEIGHT,
.psram_trans_align = 64,
.hsync_gpio_num = ESP_PANEL_LCD_PIN_NUM_RGB_HSYNC,
.vsync_gpio_num = ESP_PANEL_LCD_PIN_NUM_RGB_VSYNC,
.de_gpio_num = ESP_PANEL_LCD_PIN_NUM_RGB_DE,
.pclk_gpio_num = ESP_PANEL_LCD_PIN_NUM_RGB_PCLK,
.disp_gpio_num = ESP_PANEL_LCD_PIN_NUM_RGB_DISP,
.data_gpio_nums = {
ESP_PANEL_LCD_PIN_NUM_RGB_DATA0,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA1,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA2,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA3,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA4,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA5,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA6,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA7,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA8,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA9,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA10,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA11,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA12,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA13,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA14,
ESP_PANEL_LCD_PIN_NUM_RGB_DATA15,
},
.flags = {
.disp_active_low = 0,
.refresh_on_demand = 0,
.fb_in_psram = true,
.double_fb = true,
.no_fb = 0,
.bb_invalidate_cache = 0,
},
};
esp_lcd_new_rgb_panel(&rgb_config, &panel_handle);
// esp_lcd_rgb_panel_event_callbacks_t cbs = {
// .on_vsync = example_on_vsync_event,
// };
// esp_lcd_rgb_panel_register_event_callbacks(panel_handle, &cbs, &disp_drv);
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
}
bool example_on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data)
{
BaseType_t high_task_awoken = pdFALSE;
return high_task_awoken == pdTRUE;
}
void LCD_Init() {
ST7701_Reset();
ST7701_Init();
Touch_Init();
Backlight_Init();
}
void LCD_addWindow(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yend,uint8_t* color) {
Xend = Xend + 1; // esp_lcd_panel_draw_bitmap: x_end End index on x-axis (x_end not included)
Yend = Yend + 1; // esp_lcd_panel_draw_bitmap: y_end End index on y-axis (y_end not included)
if (Xend >= ESP_PANEL_LCD_WIDTH)
Xend = ESP_PANEL_LCD_WIDTH;
if (Yend >= ESP_PANEL_LCD_HEIGHT)
Yend = ESP_PANEL_LCD_HEIGHT;
esp_lcd_panel_draw_bitmap(panel_handle, Xstart, Ystart, Xend, Yend, color); // x_end End index on x-axis (x_end not included)
}
// backlight
uint8_t LCD_Backlight = 50;
void Backlight_Init()
{
ledcAttach(LCD_Backlight_PIN, Frequency, Resolution);
Set_Backlight(LCD_Backlight); //0~100
}
void Set_Backlight(uint8_t Light) //
{
if(Light > Backlight_MAX || Light < 0)
printf("Set Backlight parameters in the range of 0 to 100 \r\n");
else{
uint32_t Backlight = Light*10;
if(Backlight == 1000)
Backlight = 1024;
ledcWrite(LCD_Backlight_PIN, Backlight);
}
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_rgb.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "freertos/task.h"
#include "TCA9554PWR.h"
#include "LVGL_Driver.h"
#include "Touch_CST820.h"
#define LCD_CLK_PIN 2
#define LCD_MOSI_PIN 1
#define LCD_Backlight_PIN 6
// Backlight
#define PWM_Channel 1 // PWM Channel
#define Frequency 20000 // PWM frequencyconst
#define Resolution 10 // PWM resolution ratio MAX:13
#define Dutyfactor 500 // PWM Dutyfactor
#define Backlight_MAX 100
#define ESP_PANEL_LCD_WIDTH (480)
#define ESP_PANEL_LCD_HEIGHT (480)
#define ESP_PANEL_LCD_COLOR_BITS (16)
#define ESP_PANEL_LCD_RGB_PIXEL_BITS (16) // 24 | 16
#define ESP_PANEL_LCD_RGB_DATA_WIDTH (16)
#define ESP_PANEL_LCD_RGB_TIMING_FREQ_HZ (16 * 1000 * 1000)
#define ESP_PANEL_LCD_RGB_TIMING_HPW (8)
#define ESP_PANEL_LCD_RGB_TIMING_HBP (10)
#define ESP_PANEL_LCD_RGB_TIMING_HFP (50)
#define ESP_PANEL_LCD_RGB_TIMING_VPW (3)
#define ESP_PANEL_LCD_RGB_TIMING_VBP (8)
#define ESP_PANEL_LCD_RGB_TIMING_VFP (8)
#define ESP_PANEL_LCD_RGB_FRAME_BUF_NUM (2) // 1/2/3
#define ESP_PANEL_LCD_RGB_BOUNCE_BUF_SIZE (ESP_PANEL_LCD_WIDTH * 10) // Bounce buffer size in bytes. This function is used to avoid screen drift.
// To enable the bounce buffer, set it to a non-zero value. Typically set to `ESP_PANEL_LCD_WIDTH * 10`
// The size of the Bounce Buffer must satisfy `width_of_lcd * height_of_lcd = size_of_buffer * N`,
// where N is an even number.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// Please update the following configuration according to your board spec ////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define ESP_PANEL_LCD_PIN_NUM_RGB_HSYNC (38)
#define ESP_PANEL_LCD_PIN_NUM_RGB_VSYNC (39)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DE (40)
#define ESP_PANEL_LCD_PIN_NUM_RGB_PCLK (41)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA0 (5)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA1 (45)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA2 (48)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA3 (47)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA4 (21)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA5 (14)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA6 (13)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA7 (12)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA8 (11)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA9 (10)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA10 (9)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA11 (46)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA12 (3)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA13 (8)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA14 (18)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DATA15 (17)
#define ESP_PANEL_LCD_PIN_NUM_RGB_DISP (-1)
#define ESP_PANEL_LCD_BK_LIGHT_ON_LEVEL (1)
#define ESP_PANEL_LCD_BK_LIGHT_OFF_LEVEL !ESP_PANEL_LCD_BK_LIGHT_ON_LEVEL
#define EXAMPLE_ENABLE_PRINT_LCD_FPS (0)
extern uint8_t LCD_Backlight;
extern esp_lcd_panel_handle_t panel_handle;
bool example_on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data);
void ST7701_Init();
void LCD_Init();
void LCD_addWindow(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yend,uint8_t* color);
// backlight
void Backlight_Init();
void Set_Backlight(uint8_t Light);
+174
View File
@@ -0,0 +1,174 @@
#include "EyeScene.h"
#include "FreddyIris.h"
#include <cmath>
#include <lvgl.h>
#ifndef EYE_BOARD_NUMBER
#define EYE_BOARD_NUMBER 1
#endif
namespace {
constexpr int kDisplaySize = 480;
constexpr int kIrisSize = 178;
constexpr int kIrisRadius = kIrisSize / 2;
constexpr uint32_t kBlinkPeriodMs = 4600;
constexpr uint32_t kBlinkDurationMs = 300;
lv_obj_t *iris = nullptr;
lv_obj_t *red_iris = nullptr;
lv_obj_t *red_halo = nullptr;
lv_obj_t *sclera = nullptr;
lv_obj_t *upper_lid = nullptr;
lv_obj_t *lower_lid = nullptr;
lv_obj_t *status_label = nullptr;
bool tracking_red = false;
bool gaze_valid = false;
uint16_t gaze_x = 500;
uint16_t gaze_y = 500;
float drawn_x = 240.0f;
float drawn_y = 240.0f;
lv_obj_t *make_circle(lv_obj_t *parent, int size, uint32_t color) {
lv_obj_t *obj = lv_obj_create(parent);
lv_obj_set_size(obj, size, size);
lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_bg_color(obj, lv_color_hex(color), 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_outline_width(obj, 0, 0);
lv_obj_set_style_shadow_width(obj, 0, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
return obj;
}
lv_obj_t *make_lid(lv_obj_t *parent) {
lv_obj_t *obj = lv_obj_create(parent);
lv_obj_set_style_radius(obj, 0, 0);
lv_obj_set_style_bg_color(obj, lv_color_hex(0x743824), 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(obj, 0, 0);
lv_obj_set_style_outline_width(obj, 0, 0);
lv_obj_set_style_shadow_width(obj, 0, 0);
lv_obj_set_style_pad_all(obj, 0, 0);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
return obj;
}
lv_obj_t *make_red_layer(lv_obj_t *parent, int size, uint32_t color,
lv_opa_t opacity) {
lv_obj_t *layer = make_circle(parent, size, color);
lv_obj_set_style_bg_opa(layer, opacity, 0);
lv_obj_set_pos(layer, (kIrisSize - size) / 2, (kIrisSize - size) / 2);
return layer;
}
void eye_tick(lv_timer_t *) {
const uint32_t elapsed_ms = lv_tick_get();
const float seconds = static_cast<float>(elapsed_ms) / 1000.0f;
// Easing continues between camera updates and back into idle after loss.
const float desired_x = gaze_valid
? 240.0f + (static_cast<int>(gaze_x) - 500) * 0.104f
: 240.0f + 52.0f * sinf(seconds * 0.86f);
const float desired_y = gaze_valid
? 240.0f + (static_cast<int>(gaze_y) - 500) * 0.062f
: 240.0f + 31.0f * sinf(seconds * 0.53f + 0.8f);
drawn_x += (desired_x - drawn_x) * 0.20f;
drawn_y += (desired_y - drawn_y) * 0.20f;
const int iris_x = static_cast<int>(drawn_x);
const int iris_y = static_cast<int>(drawn_y);
lv_obj_set_pos(iris, iris_x - kIrisRadius, iris_y - kIrisRadius);
lv_obj_set_pos(red_iris, iris_x - kIrisRadius, iris_y - kIrisRadius);
if (tracking_red) {
const int pulse = static_cast<int>(14.0f * sinf(seconds * 4.5f));
lv_obj_set_style_bg_opa(red_halo, 55 + pulse, 0);
}
const uint32_t phase_ms = elapsed_ms % kBlinkPeriodMs;
int lid_height = 1;
if (phase_ms < kBlinkDurationMs) {
const float phase = static_cast<float>(phase_ms) / kBlinkDurationMs;
const float closure = sinf(phase * 3.14159265f);
lid_height += static_cast<int>(240.0f * closure);
}
lv_obj_set_size(upper_lid, kDisplaySize, lid_height);
lv_obj_set_size(lower_lid, kDisplaySize, lid_height);
lv_obj_set_pos(lower_lid, 0, kDisplaySize - lid_height);
}
void set_tracking_red(bool enabled) {
if (tracking_red == enabled) return;
tracking_red = enabled;
if (enabled) {
lv_obj_add_flag(iris, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(red_iris, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_bg_color(sclera, lv_color_hex(0x21191B), 0);
} else {
lv_obj_add_flag(red_iris, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(iris, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_bg_color(sclera, lv_color_hex(0xF5F4EA), 0);
}
}
} // namespace
void EyeScene_SetGaze(uint16_t x, uint16_t y, bool valid) {
if (gaze_valid != valid && status_label) {
#if EYE_BOARD_NUMBER == 2
lv_label_set_text(status_label, valid ? "BOARD 2 TRACK" : "BOARD 2 IDLE");
#else
lv_label_set_text(status_label, valid ? "BOARD 1 TRACK" : "BOARD 1 IDLE");
#endif
}
gaze_x = x;
gaze_y = y;
gaze_valid = valid;
set_tracking_red(valid);
}
void EyeScene_Create() {
lv_obj_t *screen = lv_scr_act();
lv_obj_clean(screen);
lv_obj_set_style_bg_color(screen, lv_color_hex(0x39271D), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *sclera_outline = make_circle(screen, 444, 0x743824);
lv_obj_set_pos(sclera_outline, 18, 18);
sclera = make_circle(screen, 432, 0xF5F4EA);
lv_obj_set_pos(sclera, 24, 24);
iris = lv_img_create(screen);
lv_img_set_src(iris, &kFreddyIris);
lv_obj_clear_flag(iris, LV_OBJ_FLAG_SCROLLABLE);
red_iris = lv_obj_create(screen);
lv_obj_set_size(red_iris, kIrisSize, kIrisSize);
lv_obj_set_style_bg_opa(red_iris, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(red_iris, 0, 0);
lv_obj_set_style_pad_all(red_iris, 0, 0);
lv_obj_clear_flag(red_iris, LV_OBJ_FLAG_SCROLLABLE);
make_red_layer(red_iris, kIrisSize, 0x150909, LV_OPA_COVER);
make_red_layer(red_iris, 144, 0x6E0B08, 34);
red_halo = make_red_layer(red_iris, 116, 0xBE1B13, 55);
make_red_layer(red_iris, 82, 0xE22D1E, 90);
make_red_layer(red_iris, 48, 0xFA412B, 165);
make_red_layer(red_iris, 28, 0xFF3A2B, LV_OPA_COVER);
make_red_layer(red_iris, 13, 0xFFF2E3, LV_OPA_COVER);
lv_obj_add_flag(red_iris, LV_OBJ_FLAG_HIDDEN);
status_label = lv_label_create(screen);
#if EYE_BOARD_NUMBER == 2
lv_label_set_text(status_label, "BOARD 2 IDLE");
#else
lv_label_set_text(status_label, "BOARD 1 IDLE");
#endif
lv_obj_set_style_text_color(status_label, lv_color_hex(0x3F5361), 0);
lv_obj_align(status_label, LV_ALIGN_TOP_MID, 0, 48);
upper_lid = make_lid(screen);
lower_lid = make_lid(screen);
eye_tick(nullptr);
lv_timer_create(eye_tick, 30, nullptr);
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
// Creates a moving and blinking eye on the Waveshare 480x480 display.
void EyeScene_Create();
// Normalized camera-image coordinates. A valid target switches to the red eye;
// an invalid target returns to blue. Call from the LVGL loop task.
void EyeScene_SetGaze(uint16_t x, uint16_t y, bool valid);
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <lvgl.h>
extern const lv_img_dsc_t kFreddyIris;
+304
View File
@@ -0,0 +1,304 @@
#include "Gyro_QMI8658.h"
IMUdata Accel;
IMUdata Gyro;
uint8_t Device_addr ; // default for SD0/SA0 low, 0x6A if high
acc_scale_t acc_scale = ACC_RANGE_4G;
gyro_scale_t gyro_scale = GYR_RANGE_64DPS;
acc_odr_t acc_odr = acc_odr_norm_8000;
gyro_odr_t gyro_odr = gyro_odr_norm_8000;
sensor_state_t sensor_state = sensor_default;
lpf_t acc_lpf;
float accelScales, gyroScales;
uint8_t readings[12];
uint32_t reading_timestamp_us; // timestamp in arduino micros() time
/**
* Inialize Wire and send default configs
* @param addr I2C address of sensor, typically 0x6A or 0x6B
*/
void QMI8658_Init(void)
{
uint8_t buf[1];
Device_addr = QMI8658_L_SLAVE_ADDRESS;
I2C_Read(Device_addr, QMI8658_REVISION_ID, buf, 1);
printf("QMI8658 Device ID: %x\r\n",buf[0]); // Get chip id
setState(sensor_running);
setAccScale(acc_scale);
setAccODR(acc_odr);
setAccLPF(LPF_MODE_0);
switch (acc_scale) {
// Possible accelerometer scales (and their register bit settings) are:
// 2 Gs (00), 4 Gs (01), 8 Gs (10), and 16 Gs (11).
// Here's a bit of an algorith to calculate DPS/(ADC tick) based on that
// 2-bit value:
case ACC_RANGE_2G: accelScales = 2.0 / 32768.0; break;
case ACC_RANGE_4G: accelScales = 4.0 / 32768.0; break;
case ACC_RANGE_8G: accelScales = 8.0 / 32768.0; break;
case ACC_RANGE_16G: accelScales = 16.0 / 32768.0; break;
}
setGyroScale(gyro_scale);
setGyroODR(gyro_odr);
setGyroLPF(LPF_MODE_3);
switch (gyro_scale) {
// Possible gyro scales (and their register bit settings) are:
// 250 DPS (00), 500 DPS (01), 1000 DPS (10), and 2000 DPS (11).
// Here's a bit of an algorith to calculate DPS/(ADC tick) based on that
// 2-bit value:
case GYR_RANGE_16DPS: gyroScales = 16.0 / 32768.0; break;
case GYR_RANGE_32DPS: gyroScales = 32.0 / 32768.0; break;
case GYR_RANGE_64DPS: gyroScales = 64.0 / 32768.0; break;
case GYR_RANGE_128DPS: gyroScales = 128.0 / 32768.0; break;
case GYR_RANGE_256DPS: gyroScales = 256.0 / 32768.0; break;
case GYR_RANGE_512DPS: gyroScales = 512.0 / 32768.0; break;
case GYR_RANGE_1024DPS: gyroScales = 1024.0 / 32768.0; break;
}
}
void QMI8658_Loop(void)
{
getAccelerometer();
}
/**
* Transmit one uint8_t of data to QMI8658.
* @param addr address of data to be written
* @param data the data to be written
*/
void QMI8658_transmit(uint8_t addr, uint8_t data)
{
I2C_Write(Device_addr, addr, &data, 1);
}
/**
* Receive one uint8_t of data from QMI8658.
* @param addr address of data to be read
* @return the uint8_t of data that was read
*/
uint8_t QMI8658_receive(uint8_t addr)
{
uint8_t retval;
I2C_Read(Device_addr, addr, &retval, 1);
return retval;
}
/**
* Writes data to CTRL9 (command register) and waits for ACK.
* @param command the command to be executed
*/
void QMI8658_CTRL9_Write(uint8_t command)
{
// transmit command
QMI8658_transmit(QMI8658_CTRL9, command);
// wait for command to be done
while (((QMI8658_receive(QMI8658_STATUSINT)) & 0x80) == 0x00);
}
/**
* Set output data rate (ODR) of accelerometer.
* @param odr acc_odr_t variable representing new data rate
*/
void setAccODR(acc_odr_t odr)
{
if (sensor_state != sensor_default) // If the device is not in the default state
{
uint8_t ctrl2 = QMI8658_receive(QMI8658_CTRL2);
ctrl2 &= ~QMI8658_AODR_MASK; // clear previous setting
ctrl2 |= odr; // OR in new setting
QMI8658_transmit(QMI8658_CTRL2, ctrl2);
}
acc_odr = odr;
}
/**
* Set output data rate (ODR) of gyro.
* @param odr gyro_odr_t variable representing new data rate
*/
void setGyroODR(gyro_odr_t odr)
{
if (sensor_state != sensor_default)
{
uint8_t ctrl3 = QMI8658_receive(QMI8658_CTRL3);
ctrl3 &= ~QMI8658_GODR_MASK; // clear previous setting
ctrl3 |= odr; // OR in new setting
QMI8658_transmit(QMI8658_CTRL3, ctrl3);
}
gyro_odr = odr;
}
/**
* Set scale of accelerometer output.
* @param scale acc_scale_t variable representing new scale
*/
void setAccScale(acc_scale_t scale)
{
if (sensor_state != sensor_default)
{
uint8_t ctrl2 = QMI8658_receive(QMI8658_CTRL2);
ctrl2 &= ~QMI8658_ASCALE_MASK; // clear previous setting
ctrl2 |= scale << QMI8658_ASCALE_OFFSET; // OR in new setting
QMI8658_transmit(QMI8658_CTRL2, ctrl2);
}
acc_scale = scale;
}
/**
* Set scale of gyro output.
* @param scale gyro_scale_t variable representing new scale
*/
void setGyroScale(gyro_scale_t scale)
{
if (sensor_state != sensor_default)
{
uint8_t ctrl3 = QMI8658_receive(QMI8658_CTRL3);
ctrl3 &= ~QMI8658_GSCALE_MASK; // clear previous setting
ctrl3 |= scale << QMI8658_GSCALE_OFFSET; // OR in new setting
QMI8658_transmit(QMI8658_CTRL3, ctrl3);
}
gyro_scale = scale;
}
/**
* Set new low-pass filter value for accelerometer
* @param lp lpf_t variable representing new low-pass filter value
*/
void setAccLPF(lpf_t lpf)
{
if (sensor_state != sensor_default)
{
uint8_t ctrl5 = QMI8658_receive(QMI8658_CTRL5);
ctrl5 &= !QMI8658_ALPF_MASK;
ctrl5 |= lpf << QMI8658_ALPF_OFFSET;
ctrl5 |= 0x01; // turn on acc low pass filter
QMI8658_transmit(QMI8658_CTRL5, ctrl5);
}
acc_lpf = lpf;
}
/**
* Set new low-pass filter value for gyro
* @param lp lpf_t variable representing new low-pass filter value
*/
void setGyroLPF(lpf_t lpf)
{
if (sensor_state != sensor_default)
{
uint8_t ctrl5 = QMI8658_receive(QMI8658_CTRL5);
ctrl5 &= !QMI8658_GLPF_MASK;
ctrl5 |= lpf << QMI8658_GLPF_OFFSET;
ctrl5 |= 0x10; // turn on gyro low pass filter
QMI8658_transmit(QMI8658_CTRL5, ctrl5);
}
}
/**
* Set new state of QMI8658.
* @param state new state to transition to
*/
void setState(sensor_state_t state)
{
uint8_t ctrl1;
switch (state)
{
case sensor_running:
ctrl1 = QMI8658_receive(QMI8658_CTRL1);
// enable 2MHz oscillator
ctrl1 &= 0xFE;
// enable auto address increment for fast block reads
ctrl1 |= 0x40;
QMI8658_transmit(QMI8658_CTRL1, ctrl1);
// enable high speed internal clock,
// acc and gyro in full mode, and
// disable syncSample mode
QMI8658_transmit(QMI8658_CTRL7, 0x43);
// disable AttitudeEngine Motion On Demand
QMI8658_transmit(QMI8658_CTRL6, 0x00);
break;
case sensor_power_down:
// disable high speed internal clock,
// acc and gyro powered down
QMI8658_transmit(QMI8658_CTRL7, 0x00);
ctrl1 = QMI8658_receive(QMI8658_CTRL1);
// disable 2MHz oscillator
ctrl1|= 0x01;
QMI8658_transmit(QMI8658_CTRL1, ctrl1);
break;
case sensor_locking:
ctrl1 = QMI8658_receive(QMI8658_CTRL1);
// enable 2MHz oscillator
ctrl1 &= 0xFE;
// enable auto address increment for fast block reads
ctrl1 |= 0x40;
QMI8658_transmit(QMI8658_CTRL1, ctrl1);
// enable high speed internal clock,
// acc and gyro in full mode, and
// enable syncSample mode
QMI8658_transmit(QMI8658_CTRL7, 0x83);
// disable AttitudeEngine Motion On Demand
QMI8658_transmit(QMI8658_CTRL6, 0x00);
// disable internal AHB clock gating:
QMI8658_transmit(QMI8658_CAL1_L, 0x01);
QMI8658_CTRL9_Write(0x12);
// re-enable clock gating
QMI8658_transmit(QMI8658_CAL1_L, 0x00);
QMI8658_CTRL9_Write(0x12);
break;
default:
break;
}
sensor_state = state;
}
void getAccelerometer(void)
{
uint8_t buf[6];
I2C_Read(Device_addr, QMI8658_AX_L, buf, 6);
Accel.x = (float)((int16_t)((buf[1]<<8) | (buf[0])));
Accel.y = (float)((int16_t)((buf[3]<<8) | (buf[2])));
Accel.z = (float)((int16_t)((buf[5]<<8) | (buf[4])));
Accel.x = Accel.x * accelScales;
Accel.y = Accel.y * accelScales;
Accel.z = Accel.z * accelScales;
}
void getGyroscope(void)
{
uint8_t buf[6];
I2C_Read(Device_addr, QMI8658_GX_L, buf, 6);
Gyro.x = (float)((int16_t)((buf[1]<<8) | (buf[0])));
Gyro.y = (float)((int16_t)((buf[3]<<8) | (buf[2])));
Gyro.z = (float)((int16_t)((buf[5]<<8) | (buf[4])));
Gyro.x = Gyro.x * gyroScales;
Gyro.y = Gyro.y * gyroScales;
Gyro.z = Gyro.z * gyroScales;
}
+161
View File
@@ -0,0 +1,161 @@
#pragma once
#include "I2C_Driver.h"
//device address
#define QMI8658_L_SLAVE_ADDRESS (0x6B)
#define QMI8658_H_SLAVE_ADDRESS (0x6A)
#define QMI8658_WHO_AM_I 0x00 // devide identifier
#define QMI8658_REVISION_ID 0x01
#define QMI8658_CTRL1 0x02 // SPI interface and sensor enable
#define QMI8658_CTRL2 0x03 // Accelerometer settings
#define QMI8658_CTRL3 0x04 // Gyro settings
#define QMI8658_CTRL4 0x05 // reserved (we don't use this)
#define QMI8658_CTRL5 0x06 // Low-pass filter settings
#define QMI8658_CTRL6 0x07 // AttitudeEngine settings (we don't use these)
#define QMI8658_CTRL7 0x08 // Sensor enable
#define QMI8658_CTRL8 0x09 // Motion detection control (not in current lib version)
#define QMI8658_CTRL9 0x0A // Host commands (not in current lib version)
#define QMI8658_CAL1_L 0x0B // calibration 1 register, lower bits
#define QMI8658_CAL1_H 0x0C // calibration 1 register, higher bits
#define QMI8658_CAL2_L 0x0D // calibration 2 register, lower bits
#define QMI8658_CAL2_H 0x0E // calibration 2 register, higher bits
#define QMI8658_CAL3_L 0x0F // calibration 3 register, lower bits
#define QMI8658_CAL3_H 0x10 // calibration 3 register, higher bits
#define QMI8658_CAL4_L 0x11 // calibration 4 register, lower bits
#define QMI8658_CAL4_H 0x12 // calibration 4 register, higher bits
#define QMI8658_TEMP_L 0x33 // lower bits of temperature data
#define QMI8658_TEMP_H 0x34 // upper bits of temperature data
#define QMI8658_STATUSINT 0x2D // status + interrupt register
#define QMI8658_AX_L 0x35 // lower bits of x-axis acceleration
#define QMI8658_AX_H 0x36 // upper bits of x-axis acceleration
#define QMI8658_AY_L 0x37 // lower bits of y-axis acceleration
#define QMI8658_AY_H 0x38 // upper bits of y-axis acceleration
#define QMI8658_AZ_L 0x39
#define QMI8658_AZ_H 0x3A
#define QMI8658_GX_L 0x3B // lower bits of x-axis angular velocity
#define QMI8658_GX_H 0x3C // upper bits of x-axis angular velocity
#define QMI8658_GY_L 0x3D
#define QMI8658_GY_H 0x3E
#define QMI8658_GZ_L 0x3F
#define QMI8658_GZ_H 0x40
#define QMI8658_AODR_MASK 0x0F // bits in acc data rate are 1, rest are 0 (CTRL2)
#define QMI8658_GODR_MASK 0x0F // bits in gyro data rate are 1, rest are 0 (CTRL3)
#define QMI8658_ASCALE_MASK 0x70 // bits in acc scale are 1, rest are 0
#define QMI8658_GSCALE_MASK 0x70 // bits in gyro scale are 1, rest are 0
#define QMI8658_ALPF_MASK 0x06 // bits in acc low pass filter setting
#define QMI8658_GLPF_MASK 0x60 // bits in gyro low pass filter setting
#define QMI8658_ASCALE_OFFSET 4 // offset to acc scale bits
#define QMI8658_GSCALE_OFFSET 4 // offset to gyro scale bits
#define QMI8658_ALPF_OFFSET 1 // offset to acc low pass filter bits
#define QMI8658_GLPF_OFFSET 5 // offset to gyro low pass filter bits
#define QMI8658_COMM_TIMEOUT 50 // communication timeout, in ms
// delay between refreshes of sensor data in us
// applies to individual sensor readings while in locking mode
// has no effect in running mode
#define QMI8658_REFRESH_DELAY 2000
// control clock gating (necessary to use data locking)
#define QMI8658_CTRL_CMD_AHB_CLOCK_GATING 0x12
typedef enum {
acc_odr_norm_8000 = 0x0,
acc_odr_norm_4000,
acc_odr_norm_2000,
acc_odr_norm_1000,
acc_odr_norm_500,
acc_odr_norm_250,
acc_odr_norm_120,
acc_odr_norm_60,
acc_odr_norm_30,
acc_odr_lp_128 = 0xC,
acc_odr_lp_21,
acc_odr_lp_11,
acc_odr_lp_3,
} acc_odr_t;
typedef enum {
gyro_odr_norm_8000 = 0x0,
gyro_odr_norm_4000,
gyro_odr_norm_2000,
gyro_odr_norm_1000,
gyro_odr_norm_500,
gyro_odr_norm_250,
gyro_odr_norm_120,
gyro_odr_norm_60,
gyro_odr_norm_30
} gyro_odr_t;
typedef enum {
ACC_RANGE_2G = 0x0,
ACC_RANGE_4G,
ACC_RANGE_8G,
ACC_RANGE_16G
} acc_scale_t;
typedef enum {
GYR_RANGE_16DPS = 0x0,
GYR_RANGE_32DPS,
GYR_RANGE_64DPS,
GYR_RANGE_128DPS,
GYR_RANGE_256DPS,
GYR_RANGE_512DPS,
GYR_RANGE_1024DPS
} gyro_scale_t;
typedef enum {
LPF_MODE_0 = 0x0, //2.66% of ODR
LPF_MODE_1 = 0x2, //3.63% of ODR
LPF_MODE_2 = 0x4, //5.39% of ODR
LPF_MODE_3 = 0x6 //13.37% of ODR
} lpf_t;
typedef enum {
sensor_default,
sensor_power_down,
sensor_running,
sensor_locking
} sensor_state_t;
typedef struct __IMUdata {
float x;
float y;
float z;
} IMUdata;
extern IMUdata Accel;
extern IMUdata Gyro;
void QMI8658_Init(void);
void QMI8658_Loop(void);
void QMI8658_transmit(uint8_t addr, uint8_t data);
uint8_t QMI8658_receive(uint8_t addr);
void QMI8658_CTRL9_Write(uint8_t command);
void QMI8658_sensor_update();
void QMI8658_update_if_needed();
void setAccODR(acc_odr_t odr);
void setGyroODR(gyro_odr_t odr);
void setAccScale(acc_scale_t scale);
void setGyroScale(gyro_scale_t scale);
void setAccLPF(lpf_t lpf);
void setGyroLPF(lpf_t lpf);
void setState(sensor_state_t state);
void getRawReadings(int16_t* buf);
float getAccX();
float getAccY();
float getAccZ();
float getGyroX();
float getGyroY();
float getGyroZ();
void getAccelerometer(void);
void getGyroscope(void);
+35
View File
@@ -0,0 +1,35 @@
#include "I2C_Driver.h"
void I2C_Init(void) {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
}
// 寄存器地址为 8 位的
bool I2C_Read(uint8_t Driver_addr, uint8_t Reg_addr, uint8_t *Reg_data, uint32_t Length)
{
Wire.beginTransmission(Driver_addr);
Wire.write(Reg_addr);
if ( Wire.endTransmission(true)){
printf("The I2C transmission fails. - I2C Read\r\n");
return -1;
}
Wire.requestFrom(Driver_addr, Length);
for (int i = 0; i < Length; i++) {
*Reg_data++ = Wire.read();
}
return 0;
}
bool I2C_Write(uint8_t Driver_addr, uint8_t Reg_addr, const uint8_t *Reg_data, uint32_t Length)
{
Wire.beginTransmission(Driver_addr);
Wire.write(Reg_addr);
for (int i = 0; i < Length; i++) {
Wire.write(*Reg_data++);
}
if ( Wire.endTransmission(true))
{
printf("The I2C transmission fails. - I2C Write\r\n");
return -1;
}
return 0;
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <Wire.h>
#define I2C_SCL_PIN 7
#define I2C_SDA_PIN 15
void I2C_Init(void);
bool I2C_Read(uint8_t Driver_addr, uint8_t Reg_addr, uint8_t *Reg_data, uint32_t Length);
bool I2C_Write(uint8_t Driver_addr, uint8_t Reg_addr, const uint8_t *Reg_data, uint32_t Length);
+102
View File
@@ -0,0 +1,102 @@
/*****************************************************************************
| File : LVGL_Driver.c
| help :
The provided LVGL library file must be installed first
******************************************************************************/
#include "LVGL_Driver.h"
lv_disp_drv_t disp_drv;
static lv_disp_draw_buf_t draw_buf;
void* buf1 = NULL;
void* buf2 = NULL;
// static lv_color_t buf1[ LVGL_BUF_LEN ];
// static lv_color_t buf2[ LVGL_BUF_LEN ];
// static lv_color_t* buf1 = (lv_color_t*) heap_caps_malloc(LVGL_BUF_LEN, MALLOC_CAP_SPIRAM);
// static lv_color_t* buf2 = (lv_color_t*) heap_caps_malloc(LVGL_BUF_LEN, MALLOC_CAP_SPIRAM);
/* Serial debugging */
void Lvgl_print(const char * buf)
{
// Serial.printf(buf);
// Serial.flush();
}
/* Display flushing
Displays LVGL content on the LCD
This function implements associating LVGL data to the LCD screen
*/
void Lvgl_Display_LCD( lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p )
{
LCD_addWindow(area->x1, area->y1, area->x2, area->y2, ( uint8_t *)&color_p->full);
lv_disp_flush_ready( disp_drv );
}
/*Read the touchpad*/
void Lvgl_Touchpad_Read( lv_indev_drv_t * indev_drv, lv_indev_data_t * data )
{
Touch_Read_Data();
if (touch_data.points != 0x00) {
data->point.x = touch_data.x;
data->point.y = touch_data.y;
data->state = LV_INDEV_STATE_PR;
printf("LVGL : X=%u Y=%u points=%d\r\n", touch_data.x , touch_data.y,touch_data.points);
} else {
data->state = LV_INDEV_STATE_REL;
}
if (touch_data.gesture != NONE ) {
}
touch_data.x = 0;
touch_data.y = 0;
touch_data.points = 0;
touch_data.gesture = NONE;
}
void example_increase_lvgl_tick(void *arg)
{
/* Tell LVGL how many milliseconds has elapsed */
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}
void Lvgl_Init(void)
{
lv_init();
esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2);
lv_disp_draw_buf_init( &draw_buf, buf1, buf2, ESP_PANEL_LCD_WIDTH * ESP_PANEL_LCD_HEIGHT);
/*Initialize the display*/
lv_disp_drv_init( &disp_drv );
/*Change the following line to your display resolution*/
disp_drv.hor_res = LCD_HEIGHT;
disp_drv.ver_res = LCD_WIDTH;
disp_drv.flush_cb = Lvgl_Display_LCD;
disp_drv.full_refresh = 1; /**< 1: Always make the whole screen redrawn*/
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register( &disp_drv );
/*Initialize the (dummy) input device driver*/
static lv_indev_drv_t indev_drv;
lv_indev_drv_init( &indev_drv );
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = Lvgl_Touchpad_Read;
lv_indev_drv_register( &indev_drv );
/* Create simple label */
lv_obj_t *label = lv_label_create( lv_scr_act() );
lv_label_set_text( label, "Hello Ardino and LVGL!");
lv_obj_align( label, LV_ALIGN_CENTER, 0, 0 );
const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &example_increase_lvgl_tick,
.name = "lvgl_tick"
};
esp_timer_handle_t lvgl_tick_timer = NULL;
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
}
void Lvgl_Loop(void)
{
lv_timer_handler(); /* let the GUI do its work */
// vTaskDelay(pdMS_TO_TICKS(5));
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <lvgl.h>
#include "lv_conf.h"
#include <demos/lv_demos.h>
#include <esp_heap_caps.h>
#include "Display_ST7701.h"
#include "Touch_CST820.h"
#define LCD_WIDTH ESP_PANEL_LCD_WIDTH
#define LCD_HEIGHT ESP_PANEL_LCD_HEIGHT
#define LVGL_BUF_LEN (ESP_PANEL_LCD_WIDTH * ESP_PANEL_LCD_HEIGHT / 5)
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
extern lv_disp_drv_t disp_drv;
void Lvgl_print(const char * buf);
void Lvgl_Display_LCD( lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p ); // Displays LVGL content on the LCD. This function implements associating LVGL data to the LCD screen
void Lvgl_Touchpad_Read( lv_indev_drv_t * indev_drv, lv_indev_data_t * data ); // Read the touchpad
void example_increase_lvgl_tick(void *arg);
void Lvgl_Init(void);
void Lvgl_Loop(void);
+620
View File
@@ -0,0 +1,620 @@
#include "LVGL_Example.h"
// #include "LVGL_Music.h"
#include <demos/lv_demos.h>
/**********************
* TYPEDEFS
**********************/
typedef enum {
DISP_SMALL,
DISP_MEDIUM,
DISP_LARGE,
} disp_size_t;
/**********************
* STATIC PROTOTYPES
**********************/
static void Onboard_create(lv_obj_t * parent);
static void Buzzer_create(lv_obj_t * parent);
static void Music_create(lv_obj_t * parent);
static void color_changer_create(lv_obj_t * parent);
static void color_changer_event_cb(lv_event_t * e);
static void color_event_cb(lv_event_t * e);
static void ta_event_cb(lv_event_t * e);
static void birthday_event_cb(lv_event_t * e);
static void calendar_event_cb(lv_event_t * e);
void example1_increase_lvgl_tick(lv_timer_t * t);
/**********************
* STATIC VARIABLES
**********************/
static disp_size_t disp_size;
static lv_obj_t * tv;
static lv_obj_t * calendar;
lv_style_t style_text_muted;
lv_style_t style_title;
static lv_style_t style_icon;
static lv_style_t style_bullet;
static lv_obj_t * chart1;
static lv_obj_t * chart2;
static lv_obj_t * chart3;
static lv_chart_series_t * ser1;
static lv_chart_series_t * ser2;
static lv_chart_series_t * ser3;
static lv_chart_series_t * ser4;
static const lv_font_t * font_large;
static const lv_font_t * font_normal;
static lv_timer_t * auto_step_timer;
static lv_color_t original_screen_bg_color;
static lv_timer_t * meter2_timer;
lv_obj_t * SD_Size;
lv_obj_t * FlashSize;
lv_obj_t * BAT_Volts;
lv_obj_t * Board_angle;
lv_obj_t * RTC_Time;
lv_obj_t * Wireless_Scan;
lv_obj_t * Backlight_slider;
void Lvgl_Example1(void){
if(LV_HOR_RES <= 320) disp_size = DISP_SMALL;
else if(LV_HOR_RES < 720) disp_size = DISP_MEDIUM;
else disp_size = DISP_LARGE;
font_large = LV_FONT_DEFAULT;
font_normal = LV_FONT_DEFAULT;
lv_coord_t tab_h;
if(disp_size == DISP_LARGE) {
tab_h = 70;
#if LV_FONT_MONTSERRAT_24
font_large = &lv_font_montserrat_24;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_24 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
#if LV_FONT_MONTSERRAT_16
font_normal = &lv_font_montserrat_16;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_16 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
}
else if(disp_size == DISP_MEDIUM) {
tab_h = 45;
#if LV_FONT_MONTSERRAT_20
font_large = &lv_font_montserrat_20;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_20 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
#if LV_FONT_MONTSERRAT_14
font_normal = &lv_font_montserrat_14;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_14 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
}
else { /* disp_size == DISP_SMALL */
tab_h = 45;
#if LV_FONT_MONTSERRAT_18
font_large = &lv_font_montserrat_18;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_18 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
#if LV_FONT_MONTSERRAT_12
font_normal = &lv_font_montserrat_12;
#else
LV_LOG_WARN("LV_FONT_MONTSERRAT_12 is not enabled for the widgets demo. Using LV_FONT_DEFAULT instead.");
#endif
} // 设置字体
lv_style_init(&style_text_muted);
lv_style_set_text_opa(&style_text_muted, LV_OPA_90);
lv_style_init(&style_title);
lv_style_set_text_font(&style_title, font_large);
lv_style_init(&style_icon);
lv_style_set_text_color(&style_icon, lv_theme_get_color_primary(NULL));
lv_style_set_text_font(&style_icon, font_large);
lv_style_init(&style_bullet);
lv_style_set_border_width(&style_bullet, 0);
lv_style_set_radius(&style_bullet, LV_RADIUS_CIRCLE);
tv = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, tab_h);
lv_obj_set_style_text_font(lv_scr_act(), font_normal, 0);
if(disp_size == DISP_LARGE) {
lv_obj_t * tab_btns = lv_tabview_get_tab_btns(tv);
lv_obj_set_style_pad_left(tab_btns, LV_HOR_RES / 2, 0);
lv_obj_t * logo = lv_img_create(tab_btns);
LV_IMG_DECLARE(img_lvgl_logo);
lv_img_set_src(logo, &img_lvgl_logo);
lv_obj_align(logo, LV_ALIGN_LEFT_MID, -LV_HOR_RES / 2 + 25, 0);
lv_obj_t * label = lv_label_create(tab_btns);
lv_obj_add_style(label, &style_title, 0);
lv_label_set_text(label, "LVGL v8");
lv_obj_align_to(label, logo, LV_ALIGN_OUT_RIGHT_TOP, 10, 0);
label = lv_label_create(tab_btns);
lv_label_set_text(label, "Widgets demo");
lv_obj_add_style(label, &style_text_muted, 0);
lv_obj_align_to(label, logo, LV_ALIGN_OUT_RIGHT_BOTTOM, 10, 0);
}
lv_obj_t * t1 = lv_tabview_add_tab(tv, "Onboard");
// lv_obj_t * t2 = lv_tabview_add_tab(tv, "Buzzer");
// lv_obj_t * t3 = lv_tabview_add_tab(tv, "Shop");
// lv_coord_t screen_width = lv_obj_get_width(lv_scr_act());
// lv_obj_set_width(t1, screen_width);
Onboard_create(t1);
// Buzzer_create(t2);
// shop_create(t3);
// color_changer_create(tv);
}
static void led_event_cb(lv_event_t *e) {
lv_obj_t *led = (lv_obj_t *)lv_event_get_user_data(e);
lv_obj_t *sw = lv_event_get_target(e);
if (lv_obj_get_state(sw) & LV_STATE_CHECKED) {
lv_led_on(led);
Set_EXIO(EXIO_PIN8,High);
}
else {
lv_led_off(led);
Set_EXIO(EXIO_PIN8,Low);
}
}
static void Buzzer_create(lv_obj_t * parent)
{
lv_obj_t *label = lv_label_create(parent);
lv_label_set_text(label, "The buzzer tes");
lv_obj_set_size(label, LV_PCT(30), LV_PCT(5));
lv_obj_align(label, LV_ALIGN_CENTER, 0, -60);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_t *led = lv_led_create(parent);
lv_obj_set_size(led, 50, 50);
lv_obj_align(led, LV_ALIGN_CENTER, -60, 0);
lv_led_off(led);
lv_obj_t *sw = lv_switch_create(parent);
lv_obj_align(sw, LV_ALIGN_CENTER, 60, 0);
lv_obj_add_event_cb(sw, led_event_cb, LV_EVENT_VALUE_CHANGED, led);
}
void Lvgl_Example1_close(void)
{
/*Delete all animation*/
lv_anim_del(NULL, NULL);
lv_timer_del(meter2_timer);
meter2_timer = NULL;
lv_obj_clean(lv_scr_act());
lv_style_reset(&style_text_muted);
lv_style_reset(&style_title);
lv_style_reset(&style_icon);
lv_style_reset(&style_bullet);
}
/**********************
* STATIC FUNCTIONS
**********************/
static void Onboard_create(lv_obj_t * parent)
{
/*Create a panel*/
lv_obj_t * panel1 = lv_obj_create(parent);
lv_obj_set_height(panel1, LV_SIZE_CONTENT);
lv_obj_t * panel1_title = lv_label_create(panel1);
lv_label_set_text(panel1_title, "Onboard parameter");
lv_obj_add_style(panel1_title, &style_title, 0);
lv_obj_t * SD_label = lv_label_create(panel1);
lv_label_set_text(SD_label, "SD Card");
lv_obj_add_style(SD_label, &style_text_muted, 0);
SD_Size = lv_textarea_create(panel1);
lv_textarea_set_one_line(SD_Size, true);
lv_textarea_set_placeholder_text(SD_Size, "SD Size");
lv_obj_add_event_cb(SD_Size, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * Flash_label = lv_label_create(panel1);
lv_label_set_text(Flash_label, "Flash Size");
lv_obj_add_style(Flash_label, &style_text_muted, 0);
FlashSize = lv_textarea_create(panel1);
lv_textarea_set_one_line(FlashSize, true);
lv_textarea_set_placeholder_text(FlashSize, "Flash Size");
lv_obj_add_event_cb(FlashSize, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * BAT_label = lv_label_create(panel1);
lv_label_set_text(BAT_label, "Battery Voltage");
lv_obj_add_style(BAT_label, &style_text_muted, 0);
BAT_Volts = lv_textarea_create(panel1);
lv_textarea_set_one_line(BAT_Volts, true);
lv_textarea_set_placeholder_text(BAT_Volts, "BAT Volts");
lv_obj_add_event_cb(BAT_Volts, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * angle_label = lv_label_create(panel1);
lv_label_set_text(angle_label, "Angular deflection");
lv_obj_add_style(angle_label, &style_text_muted, 0);
Board_angle = lv_textarea_create(panel1);
lv_textarea_set_one_line(Board_angle, true);
lv_textarea_set_placeholder_text(Board_angle, "Board angle");
lv_obj_add_event_cb(Board_angle, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * Time_label = lv_label_create(panel1);
lv_label_set_text(Time_label, "RTC Time");
lv_obj_add_style(Time_label, &style_text_muted, 0);
RTC_Time = lv_textarea_create(panel1);
lv_textarea_set_one_line(RTC_Time, true);
lv_textarea_set_placeholder_text(RTC_Time, "Display time");
lv_obj_add_event_cb(RTC_Time, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * Wireless_label = lv_label_create(panel1);
lv_label_set_text(Wireless_label, "Wireless scan");
lv_obj_add_style(Wireless_label, &style_text_muted, 0);
Wireless_Scan = lv_textarea_create(panel1);
lv_textarea_set_one_line(Wireless_Scan, true);
lv_textarea_set_placeholder_text(Wireless_Scan, "Wireless number");
lv_obj_add_event_cb(Wireless_Scan, ta_event_cb, LV_EVENT_ALL, NULL);
lv_obj_t * Backlight_label = lv_label_create(panel1);
lv_label_set_text(Backlight_label, "Backlight brightness");
lv_obj_add_style(Backlight_label, &style_text_muted, 0);
Backlight_slider = lv_slider_create(panel1);
lv_obj_add_flag(Backlight_slider, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(Backlight_slider, 200, 35);
lv_obj_set_style_radius(Backlight_slider, 3, LV_PART_KNOB); // Adjust the value for more or less rounding
lv_obj_set_style_bg_opa(Backlight_slider, LV_OPA_TRANSP, LV_PART_KNOB);
// lv_obj_set_style_pad_all(Backlight_slider, 0, LV_PART_KNOB);
lv_obj_set_style_bg_color(Backlight_slider, lv_color_hex(0xAAAAAA), LV_PART_KNOB);
lv_obj_set_style_bg_color(Backlight_slider, lv_color_hex(0xFFFFFF), LV_PART_INDICATOR);
lv_obj_set_style_outline_width(Backlight_slider, 2, LV_PART_INDICATOR);
lv_obj_set_style_outline_color(Backlight_slider, lv_color_hex(0xD3D3D3), LV_PART_INDICATOR);
lv_slider_set_range(Backlight_slider, 5, Backlight_MAX);
lv_slider_set_value(Backlight_slider, LCD_Backlight, LV_ANIM_ON);
lv_obj_add_event_cb(Backlight_slider, Backlight_adjustment_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t * panel2 = lv_obj_create(parent);
lv_obj_set_height(panel2, LV_SIZE_CONTENT);
lv_obj_t * panel2_title = lv_label_create(panel2);
lv_label_set_text(panel2_title, "The buzzer tes");
lv_obj_add_style(panel2_title, &style_title, 0);
lv_obj_t *led = lv_led_create(panel2);
lv_obj_set_size(led, 50, 50);
lv_obj_align(led, LV_ALIGN_CENTER, -60, 0);
lv_led_off(led);
lv_obj_t *sw = lv_switch_create(panel2);
lv_obj_set_size(sw, 65, 40);
lv_obj_align(sw, LV_ALIGN_CENTER, 60, 0);
lv_obj_add_event_cb(sw, led_event_cb, LV_EVENT_VALUE_CHANGED, led);
/////////////////////////////////////////////////////////
static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_main_row_dsc[] = {LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST};
/*Create the top panel*/
static lv_coord_t grid_1_col_dsc[] = {LV_GRID_FR(4), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(4), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_1_row_dsc[] = {
LV_GRID_CONTENT, /*Name*/
LV_GRID_CONTENT, /*Description*/
LV_GRID_CONTENT, /*Email*/
LV_GRID_CONTENT, /*Email*/
LV_GRID_TEMPLATE_LAST
};
static lv_coord_t grid_2_col_dsc[] = {LV_GRID_FR(1), LV_GRID_FR(5), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_2_row_dsc[] = {
LV_GRID_CONTENT, /*Title*/
5, /*Separator*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_CONTENT, /*Box title*/
40, /*Box*/
LV_GRID_TEMPLATE_LAST
};
lv_obj_set_grid_dsc_array(parent, grid_main_col_dsc, grid_main_row_dsc);
lv_obj_set_grid_cell(panel1, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_START, 0, 1);
lv_obj_set_grid_dsc_array(panel1, grid_2_col_dsc, grid_2_row_dsc);
lv_obj_set_grid_cell(panel1_title, LV_GRID_ALIGN_CENTER, 1, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_grid_cell(SD_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 2, 1);
lv_obj_set_grid_cell(SD_Size, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 3, 1);
lv_obj_set_grid_cell(Flash_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 4, 1);
lv_obj_set_grid_cell(FlashSize, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 5, 1);
lv_obj_set_grid_cell(BAT_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 6, 1);
lv_obj_set_grid_cell(BAT_Volts, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 7, 1);
lv_obj_set_grid_cell(angle_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 8, 1);
lv_obj_set_grid_cell(Board_angle, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 9, 1);
lv_obj_set_grid_cell(Time_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 10, 1);
lv_obj_set_grid_cell(RTC_Time, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 11, 1);
lv_obj_set_grid_cell(Wireless_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 12, 1);
lv_obj_set_grid_cell(Wireless_Scan, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 13, 1);
lv_obj_set_grid_cell(Backlight_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 14, 1);
lv_obj_set_grid_cell(Backlight_slider, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 15, 1);
lv_obj_set_grid_cell(panel2, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
lv_obj_set_grid_dsc_array(panel2, grid_1_col_dsc, grid_1_row_dsc);
lv_obj_set_grid_cell(panel2_title, LV_GRID_ALIGN_CENTER, 2, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_grid_cell(led, LV_GRID_ALIGN_CENTER, 1, 1, LV_GRID_ALIGN_CENTER, 2, 1);
lv_obj_set_grid_cell(sw, LV_GRID_ALIGN_CENTER, 3, 1, LV_GRID_ALIGN_CENTER, 2, 1);
auto_step_timer = lv_timer_create(example1_increase_lvgl_tick, 100, NULL);
}
void example1_increase_lvgl_tick(lv_timer_t * t)
{
char buf[100];
snprintf(buf, sizeof(buf), "%d MB\r\n", SDCard_Size);
lv_textarea_set_placeholder_text(SD_Size, buf);
snprintf(buf, sizeof(buf), "%d MB\r\n", Flash_Size);
lv_textarea_set_placeholder_text(FlashSize, buf);
snprintf(buf, sizeof(buf), "%.2f V\r\n", BAT_analogVolts);
lv_textarea_set_placeholder_text(BAT_Volts, buf);
snprintf(buf, sizeof(buf), "X:%.2f Y:%.2f Z:%.2f\r\n", Accel.x, Accel.y, Accel.z);
lv_textarea_set_placeholder_text(Board_angle, buf);
snprintf(buf, sizeof(buf), "%d.%d.%d %d:%d:%d\r\n",datetime.year,datetime.month,datetime.day,datetime.hour,datetime.minute,datetime.second);
lv_textarea_set_placeholder_text(RTC_Time, buf);
if(Scan_finish)
snprintf(buf, sizeof(buf), "WIFI: %d BLE: %d ..Scan Finish.\r\n",WIFI_NUM,BLE_NUM);
else
snprintf(buf, sizeof(buf), "WIFI: %d BLE: %d\r\n",WIFI_NUM,BLE_NUM);
lv_textarea_set_placeholder_text(Wireless_Scan, buf);
lv_slider_set_value(Backlight_slider, LCD_Backlight, LV_ANIM_ON);
LVGL_Backlight_adjustment(LCD_Backlight);
}
// static void Music_create(lv_obj_t * parent)
// {
// static lv_obj_t * ctrl;
// original_screen_bg_color = lv_obj_get_style_bg_color(parent, 0);
// lv_obj_set_style_bg_color(parent, lv_color_hex(0x343247), 0);
// ctrl = _lv_demo_music_main_create(parent);
// }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
static void color_changer_create(lv_obj_t * parent)
{
static lv_palette_t palette[] = {
LV_PALETTE_BLUE, LV_PALETTE_GREEN, LV_PALETTE_BLUE_GREY, LV_PALETTE_ORANGE,
LV_PALETTE_RED, LV_PALETTE_PURPLE, LV_PALETTE_TEAL, _LV_PALETTE_LAST
};
lv_obj_t * color_cont = lv_obj_create(parent);
lv_obj_remove_style_all(color_cont);
lv_obj_set_flex_flow(color_cont, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(color_cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_add_flag(color_cont, LV_OBJ_FLAG_FLOATING);
lv_obj_set_style_bg_color(color_cont, lv_color_white(), 0);
lv_obj_set_style_pad_right(color_cont, disp_size == DISP_SMALL ? LV_DPX(47) : LV_DPX(55), 0);
lv_obj_set_style_bg_opa(color_cont, LV_OPA_COVER, 0);
lv_obj_set_style_radius(color_cont, LV_RADIUS_CIRCLE, 0);
if(disp_size == DISP_SMALL) lv_obj_set_size(color_cont, LV_DPX(52), LV_DPX(52));
else lv_obj_set_size(color_cont, LV_DPX(60), LV_DPX(60));
lv_obj_align(color_cont, LV_ALIGN_BOTTOM_RIGHT, - LV_DPX(10), - LV_DPX(10));
uint32_t i;
for(i = 0; palette[i] != _LV_PALETTE_LAST; i++) {
lv_obj_t * c = lv_btn_create(color_cont);
lv_obj_set_style_bg_color(c, lv_palette_main(palette[i]), 0);
lv_obj_set_style_radius(c, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_opa(c, LV_OPA_TRANSP, 0);
lv_obj_set_size(c, 20, 20);
lv_obj_add_event_cb(c, color_event_cb, LV_EVENT_ALL, &palette[i]);
lv_obj_clear_flag(c, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
}
lv_obj_t * btn = lv_btn_create(parent);
lv_obj_add_flag(btn, LV_OBJ_FLAG_FLOATING | LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_color(btn, lv_color_white(), LV_STATE_CHECKED);
lv_obj_set_style_pad_all(btn, 10, 0);
lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0);
lv_obj_add_event_cb(btn, color_changer_event_cb, LV_EVENT_ALL, color_cont);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_bg_img_src(btn, LV_SYMBOL_TINT, 0);
if(disp_size == DISP_SMALL) {
lv_obj_set_size(btn, LV_DPX(42), LV_DPX(42));
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -LV_DPX(15), -LV_DPX(15));
}
else {
lv_obj_set_size(btn, LV_DPX(50), LV_DPX(50));
lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, -LV_DPX(15), -LV_DPX(15));
}
}
static void color_changer_anim_cb(void * var, int32_t v)
{
lv_obj_t * obj =(lv_obj_t *) var;
lv_coord_t max_w = lv_obj_get_width(lv_obj_get_parent(obj)) - LV_DPX(20);
lv_coord_t w;
if(disp_size == DISP_SMALL) {
w = lv_map(v, 0, 256, LV_DPX(52), max_w);
lv_obj_set_width(obj, w);
lv_obj_align(obj, LV_ALIGN_BOTTOM_RIGHT, - LV_DPX(10), - LV_DPX(10));
}
else {
w = lv_map(v, 0, 256, LV_DPX(60), max_w);
lv_obj_set_width(obj, w);
lv_obj_align(obj, LV_ALIGN_BOTTOM_RIGHT, - LV_DPX(10), - LV_DPX(10));
}
if(v > LV_OPA_COVER) v = LV_OPA_COVER;
uint32_t i;
for(i = 0; i < lv_obj_get_child_cnt(obj); i++) {
lv_obj_set_style_opa(lv_obj_get_child(obj, i), v, 0);
}
}
static void color_changer_event_cb(lv_event_t * e)
{
if(lv_event_get_code(e) == LV_EVENT_CLICKED) {
lv_obj_t * color_cont = (lv_obj_t *)lv_event_get_user_data(e);
if(lv_obj_get_width(color_cont) < LV_HOR_RES / 2) {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, color_cont);
lv_anim_set_exec_cb(&a, color_changer_anim_cb);
lv_anim_set_values(&a, 0, 256);
lv_anim_set_time(&a, 200);
lv_anim_start(&a);
}
else {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, color_cont);
lv_anim_set_exec_cb(&a, color_changer_anim_cb);
lv_anim_set_values(&a, 256, 0);
lv_anim_set_time(&a, 200);
lv_anim_start(&a);
}
}
}
static void color_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * obj = lv_event_get_target(e);
if(code == LV_EVENT_FOCUSED) {
lv_obj_t * color_cont = lv_obj_get_parent(obj);
if(lv_obj_get_width(color_cont) < LV_HOR_RES / 2) {
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, color_cont);
lv_anim_set_exec_cb(&a, color_changer_anim_cb);
lv_anim_set_values(&a, 0, 256);
lv_anim_set_time(&a, 200);
lv_anim_start(&a);
}
}
else if(code == LV_EVENT_CLICKED) {
lv_palette_t * palette_primary = (lv_palette_t *)lv_event_get_user_data(e);
lv_palette_t palette_secondary =(lv_palette_t) ((*palette_primary) + 3); /*Use another palette as secondary*/
if(palette_secondary >= _LV_PALETTE_LAST) palette_secondary =(lv_palette_t)(0);
#if LV_USE_THEME_DEFAULT
lv_theme_default_init(NULL, lv_palette_main(*palette_primary), lv_palette_main(palette_secondary),
LV_THEME_DEFAULT_DARK, font_normal);
#endif
lv_color_t color = lv_palette_main(*palette_primary);
lv_style_set_text_color(&style_icon, color);
lv_chart_set_series_color(chart1, ser1, color);
lv_chart_set_series_color(chart2, ser3, color);
}
}
static void ta_event_cb(lv_event_t * e)
{
}
static void birthday_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * ta = lv_event_get_target(e);
if(code == LV_EVENT_FOCUSED) {
if(lv_indev_get_type(lv_indev_get_act()) == LV_INDEV_TYPE_POINTER) {
if(calendar == NULL) {
lv_obj_add_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);
calendar = lv_calendar_create(lv_layer_top());
lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_50, 0);
lv_obj_set_style_bg_color(lv_layer_top(), lv_palette_main(LV_PALETTE_GREY), 0);
if(disp_size == DISP_SMALL) lv_obj_set_size(calendar, 180, 200);
else if(disp_size == DISP_MEDIUM) lv_obj_set_size(calendar, 200, 220);
else lv_obj_set_size(calendar, 300, 330);
lv_calendar_set_showed_date(calendar, 1990, 01);
lv_obj_align(calendar, LV_ALIGN_CENTER, 0, 30);
lv_obj_add_event_cb(calendar, calendar_event_cb, LV_EVENT_ALL, ta);
lv_calendar_header_dropdown_create(calendar);
}
}
}
}
static void calendar_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * ta = (lv_obj_t *)lv_event_get_user_data(e);
lv_obj_t * obj = (lv_obj_t *)lv_event_get_current_target(e);
if(code == LV_EVENT_VALUE_CHANGED) {
lv_calendar_date_t d;
lv_calendar_get_pressed_date(obj, &d);
char buf[32];
lv_snprintf(buf, sizeof(buf), "%02d.%02d.%d", d.day, d.month, d.year);
lv_textarea_set_text(ta, buf);
lv_obj_del(calendar);
calendar = NULL;
lv_obj_clear_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_TRANSP, 0);
}
}
void Backlight_adjustment_event_cb(lv_event_t * e) {
uint8_t Backlight = lv_slider_get_value(lv_event_get_target(e));
if (Backlight >= 0 && Backlight <= Backlight_MAX) {
lv_slider_set_value(Backlight_slider, Backlight, LV_ANIM_ON);
LCD_Backlight = Backlight;
LVGL_Backlight_adjustment(Backlight);
}
else
printf("Backlight out of range: %d\n", Backlight);
}
void LVGL_Backlight_adjustment(uint8_t Backlight) {
Set_Backlight(Backlight);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "LVGL_Driver.h"
#include "Gyro_QMI8658.h"
#include "RTC_PCF85063.h"
#include "SD_Card.h"
#include "Wireless.h"
#include "BAT_Driver.h"
#include "Display_ST7701.h"
#define EXAMPLE1_LVGL_TICK_PERIOD_MS 1000
void Backlight_adjustment_event_cb(lv_event_t * e);
void LVGL_Backlight_adjustment(uint8_t Backlight);
void Lvgl_Example1(void);
+168
View File
@@ -0,0 +1,168 @@
#include "OtaService.h"
#include <cstring>
#include "WiFi.h"
#include "WebServer.h"
#include "Update.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "mbedtls/sha256.h"
#include "../shared/OtaAuth.h"
#include "../shared/OtaSecrets.h"
#ifndef EYE_BOARD_NUMBER
#define EYE_BOARD_NUMBER 1
#endif
namespace {
constexpr uint16_t kPort = 3232;
#if EYE_BOARD_NUMBER == 2
constexpr char kDevice[] = "eye-2";
constexpr char kImageTarget[] = "FREDDY_OTA_TARGET:eye-2";
#else
constexpr char kDevice[] = "eye-1";
constexpr char kImageTarget[] = "FREDDY_OTA_TARGET:eye-1";
#endif
WebServer server(kPort);
char nonce[33] = {};
char expected_sha[65] = {};
uint32_t nonce_time = 0;
uint32_t retry_time = 0;
uint32_t status_time = 0;
uint32_t restart_time = 0;
size_t expected_length = 0;
bool listening = false;
bool upload_ok = false;
bool upload_started = false;
mbedtls_sha256_context sha_context;
void new_nonce() {
uint8_t random_bytes[16];
esp_fill_random(random_bytes, sizeof(random_bytes));
freddy_ota::to_hex(random_bytes, sizeof(random_bytes), nonce);
nonce_time = millis();
}
void send_info() {
new_nonce();
const esp_partition_t *slot = esp_ota_get_next_update_partition(nullptr);
const esp_partition_t *running = esp_ota_get_running_partition();
char body[180];
snprintf(body, sizeof(body),
"{\"device\":\"%s\",\"nonce\":\"%s\",\"slot_bytes\":%u,\"running_slot\":%d}",
kDevice, nonce, slot ? static_cast<unsigned>(slot->size) : 0,
running ? static_cast<int>(running->subtype) - ESP_PARTITION_SUBTYPE_APP_OTA_0 : -1);
server.send(200, "application/json", body);
}
void handle_raw() {
HTTPRaw &raw = server.raw();
if (raw.status == RAW_START) {
upload_ok = false;
upload_started = false;
const String length_text = server.header("Content-Length");
const String nonce_text = server.header("X-Freddy-Nonce");
const String sha_text = server.header("X-Freddy-SHA256");
const String signature = server.header("X-Freddy-Signature");
char *end = nullptr;
const unsigned long parsed = strtoul(length_text.c_str(), &end, 10);
const esp_partition_t *slot = esp_ota_get_next_update_partition(nullptr);
if (!slot || !end || *end || !parsed || parsed > slot->size ||
millis() - nonce_time > 60000 || nonce_text != nonce ||
!freddy_ota::verify(FREDDY_OTA_KEY, kDevice, nonce,
sha_text.c_str(), parsed, signature.c_str())) return;
expected_length = parsed;
memcpy(expected_sha, sha_text.c_str(), 65);
nonce[0] = 0; // One use, even when the transfer fails.
if (!Update.begin(expected_length)) return;
mbedtls_sha256_init(&sha_context);
if (mbedtls_sha256_starts(&sha_context, 0) != 0) {
Update.abort();
mbedtls_sha256_free(&sha_context);
return;
}
upload_started = true;
} else if (raw.status == RAW_WRITE && upload_started) {
if (Update.write(raw.buf, raw.currentSize) != raw.currentSize ||
mbedtls_sha256_update(&sha_context, raw.buf, raw.currentSize) != 0) {
Update.abort();
mbedtls_sha256_free(&sha_context);
upload_started = false;
}
} else if (raw.status == RAW_END && upload_started) {
uint8_t digest[32] = {};
char actual[65];
const bool hash_ok =
raw.totalSize == expected_length &&
mbedtls_sha256_finish(&sha_context, digest) == 0;
mbedtls_sha256_free(&sha_context);
freddy_ota::to_hex(digest, sizeof(digest), actual);
upload_ok = hash_ok &&
freddy_ota::constant_time_equal(actual, expected_sha, 64);
if (upload_ok) upload_ok = Update.end();
else Update.abort();
upload_started = false;
} else if (raw.status == RAW_ABORTED && upload_started) {
Update.abort();
mbedtls_sha256_free(&sha_context);
upload_started = false;
}
}
void finish_upload() {
if (upload_ok) {
server.send(200, "application/json", "{\"ok\":true}");
restart_time = millis() + 1000;
} else {
server.send(400, "application/json", "{\"ok\":false}");
}
}
} // namespace
void OtaService_Begin() {
if (!FREDDY_WIFI_SSID[0] || strlen(FREDDY_OTA_KEY) < 32) {
Serial.println("OTA_DISABLED: configure firmware/private/ first");
return;
}
const char *headers[] = {
"Content-Length", "X-Freddy-Nonce", "X-Freddy-SHA256",
"X-Freddy-Signature"};
server.collectHeaders(headers, 4);
server.on("/ota/info", HTTP_GET, send_info);
server.on("/ota", HTTP_POST, finish_upload, handle_raw);
WiFi.mode(WIFI_STA);
WiFi.setHostname(EYE_BOARD_NUMBER == 2 ? "freddy-eye-2" : "freddy-eye-1");
WiFi.setSleep(false);
WiFi.begin(FREDDY_WIFI_SSID, FREDDY_WIFI_PASSWORD);
retry_time = millis();
}
void OtaService_Loop() {
if (!FREDDY_WIFI_SSID[0] || strlen(FREDDY_OTA_KEY) < 32) return;
if (restart_time && static_cast<int32_t>(millis() - restart_time) >= 0)
ESP.restart();
if (millis() - status_time >= 10000) {
status_time = millis();
if (Serial) Serial.printf("OTA_WIFI device=%s status=%d ip=%s\n",
kDevice, WiFi.status(),
WiFi.localIP().toString().c_str());
}
if (WiFi.status() != WL_CONNECTED) {
if (listening) {
server.stop();
listening = false;
}
if (millis() - retry_time > 10000) {
WiFi.reconnect();
retry_time = millis();
}
return;
}
if (!listening) {
server.begin();
listening = true;
Serial.printf("OTA_READY device=%s image=%s ip=%s port=%u\n",
kDevice, kImageTarget, WiFi.localIP().toString().c_str(), kPort);
}
server.handleClient();
}
+4
View File
@@ -0,0 +1,4 @@
#pragma once
void OtaService_Begin();
void OtaService_Loop();
+229
View File
@@ -0,0 +1,229 @@
#include "RTC_PCF85063.h"
datetime_t datetime= {0};
static uint8_t decToBcd(int val);
static int bcdToDec(uint8_t val);
const unsigned char MonthStr[12][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov","Dec"};
/******************************************************************************
function: PCF85063 initialized
parameter:
Info:Initiate Normal Mode, RTC Run, NO reset, No correction , 24hr format, Internal load capacitane 12.5pf
******************************************************************************/
void PCF85063_Init()
{
uint8_t Value = RTC_CTRL_1_DEFAULT|RTC_CTRL_1_CAP_SEL;
I2C_Write(PCF85063_ADDRESS, RTC_CTRL_1_ADDR, &Value, 1);
I2C_Read(PCF85063_ADDRESS, RTC_CTRL_1_ADDR, &Value, 1);
if(Value & RTC_CTRL_1_STOP)
printf("PCF85063 failed to be initialized.state :%d\r\n",Value);
else
printf("PCF85063 is running,state :%d\r\n",Value);
// datetime_t Now_datetime= {0};
// Now_datetime.year = 2024;
// Now_datetime.month = 9;
// Now_datetime.day = 20;
// Now_datetime.dotw = 5;
// Now_datetime.hour = 9;
// Now_datetime.minute = 50;
// Now_datetime.second = 0;
// PCF85063_Set_All(Now_datetime);
}
void RTC_Loop(void)
{
PCF85063_Read_Time(&datetime);
}
/******************************************************************************
function: Reset PCF85063
parameter:
Info:
******************************************************************************/
void PCF85063_Reset()
{
uint8_t Value = RTC_CTRL_1_DEFAULT|RTC_CTRL_1_CAP_SEL|RTC_CTRL_1_SR;
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_CTRL_1_ADDR, &Value, 1);
if(ret != ESP_OK)
printf("PCF85063 : Reset failure\r\n");
}
/******************************************************************************
function: Set Time
parameter:
Info:
******************************************************************************/
void PCF85063_Set_Time(datetime_t time)
{
uint8_t buf[3] = {decToBcd(time.second),
decToBcd(time.minute),
decToBcd(time.hour)};
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_SECOND_ADDR, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Time setting failure\r\n");
}
/******************************************************************************
function: Set Date
parameter:
Info:
******************************************************************************/
void PCF85063_Set_Date(datetime_t date)
{
uint8_t buf[4] = {decToBcd(date.day),
decToBcd(date.dotw),
decToBcd(date.month),
decToBcd(date.year - YEAR_OFFSET)};
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_DAY_ADDR, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Date setting failed\r\n");
}
/******************************************************************************
function: Set Time And Date
parameter:
Info:
******************************************************************************/
void PCF85063_Set_All(datetime_t time)
{
uint8_t buf[7] = {decToBcd(time.second),
decToBcd(time.minute),
decToBcd(time.hour),
decToBcd(time.day),
decToBcd(time.dotw),
decToBcd(time.month),
decToBcd(time.year - YEAR_OFFSET)};
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_SECOND_ADDR, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Failed to set the date and time\r\n");
}
/******************************************************************************
function: Read Time And Date
parameter:
Info:
******************************************************************************/
void PCF85063_Read_Time(datetime_t *time)
{
uint8_t buf[7] = {0};
esp_err_t ret = I2C_Read(PCF85063_ADDRESS, RTC_SECOND_ADDR, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Time read failure\r\n");
else{
time->second = bcdToDec(buf[0] & 0x7F);
time->minute = bcdToDec(buf[1] & 0x7F);
time->hour = bcdToDec(buf[2] & 0x3F);
time->day = bcdToDec(buf[3] & 0x3F);
time->dotw = bcdToDec(buf[4] & 0x07);
time->month = bcdToDec(buf[5] & 0x1F);
time->year = bcdToDec(buf[6]) + YEAR_OFFSET;
}
}
/******************************************************************************
function: Enable Alarm and Clear Alarm flag
parameter:
Info:
******************************************************************************/
void PCF85063_Enable_Alarm()
{
uint8_t Value = RTC_CTRL_2_DEFAULT | RTC_CTRL_2_AIE;
Value &= ~RTC_CTRL_2_AF;
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_CTRL_2_ADDR, &Value, 1);
if(ret != ESP_OK)
printf("PCF85063 : Failed to enable Alarm Flag and Clear Alarm Flag \r\n");
}
/******************************************************************************
function: Get Alarm flag
parameter:
Info:
******************************************************************************/
uint8_t PCF85063_Get_Alarm_Flag()
{
uint8_t Value = 0;
esp_err_t ret = I2C_Read(PCF85063_ADDRESS, RTC_CTRL_2_ADDR, &Value, 1);
if(ret != ESP_OK)
printf("PCF85063 : Failed to obtain a warning flag.\r\n");
else
Value &= RTC_CTRL_2_AF | RTC_CTRL_2_AIE;
//printf("Value = 0x%x",Value);
return Value;
}
/******************************************************************************
function: Set Alarm
parameter:
Info:
******************************************************************************/
void PCF85063_Set_Alarm(datetime_t time)
{
uint8_t buf[5] ={
decToBcd(time.second)&(~RTC_ALARM),
decToBcd(time.minute)&(~RTC_ALARM),
decToBcd(time.hour)&(~RTC_ALARM),
//decToBcd(time.day)&(~RTC_ALARM),
//decToBcd(time.dotw)&(~RTC_ALARM)
RTC_ALARM, //disalbe day
RTC_ALARM //disalbe weekday
};
esp_err_t ret = I2C_Write(PCF85063_ADDRESS, RTC_SECOND_ALARM, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Failed to set alarm flag\r\n");
}
/******************************************************************************
function: Read Alarm
parameter:
Info:
******************************************************************************/
void PCF85063_Read_Alarm(datetime_t *time)
{
uint8_t buf[5] = {0};
esp_err_t ret = I2C_Read(PCF85063_ADDRESS, RTC_SECOND_ALARM, buf, sizeof(buf));
if(ret != ESP_OK)
printf("PCF85063 : Failed to read the alarm sign\r\n");
else{
time->second = bcdToDec(buf[0] & 0x7F);
time->minute = bcdToDec(buf[1] & 0x7F);
time->hour = bcdToDec(buf[2] & 0x3F);
time->day = bcdToDec(buf[3] & 0x3F);
time->dotw = bcdToDec(buf[4] & 0x07);
}
}
/******************************************************************************
function: Convert normal decimal numbers to binary coded decimal
parameter:
Info:
******************************************************************************/
static uint8_t decToBcd(int val)
{
return (uint8_t)((val / 10 * 16) + (val % 10));
}
/******************************************************************************
function: Convert binary coded decimal to normal decimal numbers
parameter:
Info:
******************************************************************************/
static int bcdToDec(uint8_t val)
{
return (int)((val / 16 * 10) + (val % 16));
}
/******************************************************************************
function:
parameter:
Info:
******************************************************************************/
void datetime_to_str(char *datetime_str,datetime_t time)
{
sprintf(datetime_str, " %d.%d.%d %d %d:%d:%d ", time.year, time.month,
time.day, time.dotw, time.hour, time.minute, time.second);
}
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include "I2C_Driver.h"
//PCF85063_ADDRESS
#define PCF85063_ADDRESS (0x51)
//
#define YEAR_OFFSET (1970)
// registar overview - crtl & status reg
#define RTC_CTRL_1_ADDR (0x00)
#define RTC_CTRL_2_ADDR (0x01)
#define RTC_OFFSET_ADDR (0x02)
#define RTC_RAM_by_ADDR (0x03)
// registar overview - time & data reg
#define RTC_SECOND_ADDR (0x04)
#define RTC_MINUTE_ADDR (0x05)
#define RTC_HOUR_ADDR (0x06)
#define RTC_DAY_ADDR (0x07)
#define RTC_WDAY_ADDR (0x08)
#define RTC_MONTH_ADDR (0x09)
#define RTC_YEAR_ADDR (0x0A) // years 0-99; calculate real year = 1970 + RCC reg year
// registar overview - alarm reg
#define RTC_SECOND_ALARM (0x0B)
#define RTC_MINUTE_ALARM (0x0C)
#define RTC_HOUR_ALARM (0x0D)
#define RTC_DAY_ALARM (0x0E)
#define RTC_WDAY_ALARM (0x0F)
// registar overview - timer reg
#define RTC_TIMER_VAL (0x10)
#define RTC_TIMER_MODE (0x11)
//RTC_CTRL_1 registar
#define RTC_CTRL_1_EXT_TEST (0x80)
#define RTC_CTRL_1_STOP (0x20) //0-RTC clock runs 1- RTC clock is stopped
#define RTC_CTRL_1_SR (0X10) //0-no software reset 1-initiate software rese
#define RTC_CTRL_1_CIE (0X04) //0-no correction interrupt generated 1-interrupt pulses are generated at every correction cycle
#define RTC_CTRL_1_12_24 (0X02) //0-24H 1-12H
#define RTC_CTRL_1_CAP_SEL (0X01) //0-7PF 1-12.5PF
//RTC_CTRL_2 registar
#define RTC_CTRL_2_AIE (0X80) //alarm interrupt 0-disalbe 1-enable
#define RTC_CTRL_2_AF (0X40) //alarm flag 0-inactive/cleared 1-active/unchanged
#define RTC_CTRL_2_MI (0X20) //minute interrupt 0-disalbe 1-enable
#define RTC_CTRL_2_HMI (0X10) //half minute interrupt
#define RTC_CTRL_2_TF (0X08)
//
#define RTC_OFFSET_MODE (0X80)
//
#define RTC_TIMER_MODE_TE (0X04) //timer enable 0-disalbe 1-enable
#define RTC_TIMER_MODE_TIE (0X02) //timer interrupt enable 0-disalbe 1-enable
#define RTC_TIMER_MODE_TI_TP (0X01) //timer interrupt mode 0-interrupt follows timer flag 1-interrupt generates a pulse
// format
#define RTC_ALARM (0x80) // set AEN_x registers
#define RTC_CTRL_1_DEFAULT (0x00)
#define RTC_CTRL_2_DEFAULT (0x00)
#define RTC_TIMER_FLAG (0x08)
typedef struct {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t dotw;
uint8_t hour;
uint8_t minute;
uint8_t second;
}datetime_t;
extern datetime_t datetime;
void PCF85063_Init(void);
void RTC_Loop(void);
void PCF85063_Reset(void);
void PCF85063_Set_Time(datetime_t time);
void PCF85063_Set_Date(datetime_t date);
void PCF85063_Set_All(datetime_t time);
void PCF85063_Read_Time(datetime_t *time);
void PCF85063_Enable_Alarm(void);
uint8_t PCF85063_Get_Alarm_Flag();
void PCF85063_Set_Alarm(datetime_t time);
void PCF85063_Read_Alarm(datetime_t *time);
void datetime_to_str(char *datetime_str,datetime_t time);
// weekday format
// 0 - sunday
// 1 - monday
// 2 - tuesday
// 3 - wednesday
// 4 - thursday
// 5 - friday
// 6 - saturday
+122
View File
@@ -0,0 +1,122 @@
#include "SD_Card.h"
bool SDCard_Flag;
bool SDCard_Finish;
uint16_t SDCard_Size;
uint16_t Flash_Size;
void SD_D3_Dis(){
Set_EXIO(EXIO_PIN4,Low);
vTaskDelay(pdMS_TO_TICKS(10));
}
void SD_D3_EN(){
Set_EXIO(EXIO_PIN4,High);
vTaskDelay(pdMS_TO_TICKS(10));
}
void SD_Init() {
// SD MMC
if(!SD_MMC.setPins(SD_CLK_PIN, SD_CMD_PIN, SD_D0_PIN,-1,-1,-1)){
printf("SD MMC: Pin change failed!\r\n");
return;
}
SD_D3_EN();
if (SD_MMC.begin("/sdcard", true, true)) { // "/sdcard", true, true or "/sdcard", true, false
printf("SD card initialization successful!\r\n");
} else {
printf("SD card initialization failed!\r\n");
}
uint8_t cardType = SD_MMC.cardType();
if(cardType == CARD_NONE){
printf("No SD card attached\r\n");
return;
}
else{
printf("SD Card Type: ");
if(cardType == CARD_MMC){
printf("MMC\r\n");
} else if(cardType == CARD_SD){
printf("SDSC\r\n");
} else if(cardType == CARD_SDHC){
printf("SDHC\r\n");
} else {
printf("UNKNOWN\r\n");
}
uint64_t totalBytes = SD_MMC.totalBytes();
uint64_t usedBytes = SD_MMC.usedBytes();
SDCard_Size = totalBytes/(1024*1024);
printf("Total space: %llu\n", totalBytes);
printf("Used space: %llu\n", usedBytes);
printf("Free space: %llu\n", totalBytes - usedBytes);
}
}
bool File_Search(const char* directory, const char* fileName)
{
File Path = SD_MMC.open(directory);
if (!Path) {
printf("Path: <%s> does not exist\r\n",directory);
return false;
}
File file = Path.openNextFile();
while (file) {
if (strcmp(file.name(), fileName) == 0) {
if (strcmp(directory, "/") == 0)
printf("File '%s%s' found in root directory.\r\n",directory,fileName);
else
printf("File '%s/%s' found in root directory.\r\n",directory,fileName);
Path.close();
return true;
}
file = Path.openNextFile();
}
if (strcmp(directory, "/") == 0)
printf("File '%s%s' not found in root directory.\r\n",directory,fileName);
else
printf("File '%s/%s' not found in root directory.\r\n",directory,fileName);
Path.close();
return false;
}
uint16_t Folder_retrieval(const char* directory, const char* fileExtension, char File_Name[][100],uint16_t maxFiles)
{
File Path = SD_MMC.open(directory);
if (!Path) {
printf("Path: <%s> does not exist\r\n",directory);
return false;
}
uint16_t fileCount = 0;
char filePath[100];
File file = Path.openNextFile();
while (file && fileCount < maxFiles) {
if (!file.isDirectory() && strstr(file.name(), fileExtension)) {
strncpy(File_Name[fileCount], file.name(), sizeof(File_Name[fileCount]));
if (strcmp(directory, "/") == 0) {
snprintf(filePath, 100, "%s%s", directory, file.name());
} else {
snprintf(filePath, 100, "%s/%s", directory, file.name());
}
printf("File found: %s\r\n", filePath);
fileCount++;
}
file = Path.openNextFile();
}
Path.close();
if (fileCount > 0) {
printf("Retrieved %d mp3 files\r\n",fileCount);
return fileCount;
} else {
printf("No files with extension '%s' found in directory: %s\r\n", fileExtension, directory);
return 0;
}
}
void Flash_test()
{
printf("/********** RAM Test**********/\r\n");
// Get Flash size
uint32_t flashSize = ESP.getFlashChipSize();
Flash_Size = flashSize/1024/1024;
printf("Flash size: %d MB \r\n", flashSize/1024/1024);
printf("/******* RAM Test Over********/\r\n\r\n");
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "Arduino.h"
#include <cstring>
#include "FS.h"
#include "SD_MMC.h"
#include "TCA9554PWR.h"
#define SD_CLK_PIN 2
#define SD_CMD_PIN 1
#define SD_D0_PIN 42
extern uint16_t SDCard_Size;
extern uint16_t Flash_Size;
void SD_Init();
void Flash_test();
bool File_Search(const char* directory, const char* fileName);
uint16_t Folder_retrieval(const char* directory, const char* fileExtension, char File_Name[][100],uint16_t maxFiles);
+96
View File
@@ -0,0 +1,96 @@
#include "TCA9554PWR.h"
/***************************************************** Operation register REG ****************************************************/
uint8_t I2C_Read_EXIO(uint8_t REG) // Read the value of the TCA9554PWR register REG
{
Wire.beginTransmission(TCA9554_ADDRESS);
Wire.write(REG);
uint8_t result = Wire.endTransmission();
if (result != 0) {
printf("The I2C transmission fails. - I2C Read EXIO\r\n");
}
Wire.requestFrom(TCA9554_ADDRESS, 1);
uint8_t bitsStatus;
if (Wire.available()) {
bitsStatus = Wire.read();
}
return bitsStatus;
}
uint8_t I2C_Write_EXIO(uint8_t REG,uint8_t Data) // Write Data to the REG register of the TCA9554PWR
{
Wire.beginTransmission(TCA9554_ADDRESS);
Wire.write(REG);
Wire.write(Data);
uint8_t result = Wire.endTransmission();
if (result != 0) {
printf("The I2C transmission fails. - I2C Write EXIO\r\n");
return -1;
}
return 0;
}
/********************************************************** Set EXIO mode **********************************************************/
void Mode_EXIO(uint8_t Pin,uint8_t State) // Set the mode of the TCA9554PWR Pin. The default is Output mode (output mode or input mode). State: 0= Output mode 1= input mode
{
uint8_t bitsStatus = I2C_Read_EXIO(TCA9554_CONFIG_REG);
uint8_t Data = (0x01 << (Pin-1)) | bitsStatus;
uint8_t result = I2C_Write_EXIO(TCA9554_CONFIG_REG,Data);
if (result != 0) {
printf("I/O Configuration Failure !!!\r\n");
}
}
void Mode_EXIOS(uint8_t PinState) // Set the mode of the 7 pins from the TCA9554PWR with PinState
{
uint8_t result = I2C_Write_EXIO(TCA9554_CONFIG_REG,PinState);
if (result != 0) {
printf("I/O Configuration Failure !!!\r\n");
}
}
/********************************************************** Read EXIO status **********************************************************/
uint8_t Read_EXIO(uint8_t Pin) // Read the level of the TCA9554PWR Pin
{
uint8_t inputBits = I2C_Read_EXIO(TCA9554_INPUT_REG);
uint8_t bitStatus = (inputBits >> (Pin-1)) & 0x01;
return bitStatus;
}
uint8_t Read_EXIOS(uint8_t REG = TCA9554_INPUT_REG) // Read the level of all pins of TCA9554PWR, the default read input level state, want to get the current IO output state, pass the parameter TCA9554_OUTPUT_REG, such as Read_EXIOS(TCA9554_OUTPUT_REG);
{
uint8_t inputBits = I2C_Read_EXIO(REG);
return inputBits;
}
/********************************************************** Set the EXIO output status **********************************************************/
void Set_EXIO(uint8_t Pin,uint8_t State) // Sets the level state of the Pin without affecting the other pins
{
uint8_t Data;
if(State < 2 && Pin < 9 && Pin > 0){
uint8_t bitsStatus = Read_EXIOS(TCA9554_OUTPUT_REG);
if(State == 1)
Data = (0x01 << (Pin-1)) | bitsStatus;
else if(State == 0)
Data = (~(0x01 << (Pin-1))) & bitsStatus;
uint8_t result = I2C_Write_EXIO(TCA9554_OUTPUT_REG,Data);
if (result != 0) {
printf("Failed to set GPIO!!!\r\n");
}
}
else
printf("Parameter error, please enter the correct parameter!\r\n");
}
void Set_EXIOS(uint8_t PinState) // Set 7 pins to the PinState state such as :PinState=0x23, 0010 0011 state (the highest bit is not used)
{
uint8_t result = I2C_Write_EXIO(TCA9554_OUTPUT_REG,PinState);
if (result != 0) {
printf("Failed to set GPIO!!!\r\n");
}
}
/********************************************************** Flip EXIO state **********************************************************/
void Set_Toggle(uint8_t Pin) // Flip the level of the TCA9554PWR Pin
{
uint8_t bitsStatus = Read_EXIO(Pin);
Set_EXIO(Pin,(bool)!bitsStatus);
}
/********************************************************* TCA9554PWR Initializes the device ***********************************************************/
void TCA9554PWR_Init(uint8_t PinState) // Set the seven pins to PinState state, for example :PinState=0x23, 0010 0011 State (Output mode or input mode) 0= Output mode 1= Input mode. The default value is output mode
{
Mode_EXIOS(PinState);
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <stdio.h>
#include "I2C_Driver.h"
#define TCA9554_EXIO1 0x01
#define TCA9554_EXIO2 0x02
#define TCA9554_EXIO3 0x03
#define TCA9554_EXIO4 0x04
#define TCA9554_EXIO5 0x05
#define TCA9554_EXIO6 0x06
#define TCA9554_EXIO7 0x07
/****************************************************** The macro defines the TCA9554PWR information ******************************************************/
#define TCA9554_ADDRESS 0x20 // TCA9554PWR I2C address
// TCA9554PWR寄存器地址
#define TCA9554_INPUT_REG 0x00 // Input register,input level
#define TCA9554_OUTPUT_REG 0x01 // Output register, high and low level output
#define TCA9554_Polarity_REG 0x02 // The Polarity Inversion register (register 2) allows polarity inversion of pins defined as inputs by the Configuration register.
#define TCA9554_CONFIG_REG 0x03 // Configuration register, mode configuration
#define Low 0
#define High 1
#define EXIO_PIN1 1
#define EXIO_PIN2 2
#define EXIO_PIN3 3
#define EXIO_PIN4 4
#define EXIO_PIN5 5
#define EXIO_PIN6 6
#define EXIO_PIN7 7
#define EXIO_PIN8 8
/***************************************************** Operation register REG ****************************************************/
uint8_t I2C_Read_EXIO(uint8_t REG); // Read the value of the TCA9554PWR register REG
uint8_t I2C_Write_EXIO(uint8_t REG,uint8_t Data); // Write Data to the REG register of the TCA9554PWR
/********************************************************** Set EXIO mode **********************************************************/
void Mode_EXIO(uint8_t Pin,uint8_t State); // Set the mode of the TCA9554PWR Pin. The default is Output mode (output mode or input mode). State: 0= Output mode 1= input mode
void Mode_EXIOS(uint8_t PinState); // Set the mode of the 7 pins from the TCA9554PWR with PinState
/********************************************************** Read EXIO status **********************************************************/
uint8_t Read_EXIO(uint8_t Pin); // Read the level of the TCA9554PWR Pin
uint8_t Read_EXIOS(uint8_t REG); // Read the level of all pins of TCA9554PWR, the default read input level state, want to get the current IO output state, pass the parameter TCA9554_OUTPUT_REG, such as Read_EXIOS(TCA9554_OUTPUT_REG);
/********************************************************** Set the EXIO output status **********************************************************/
void Set_EXIO(uint8_t Pin,uint8_t State); // Sets the level state of the Pin without affecting the other pins
void Set_EXIOS(uint8_t PinState); // Set 7 pins to the PinState state such as :PinState=0x23, 0010 0011 state (the highest bit is not used)
/********************************************************** Flip EXIO state **********************************************************/
void Set_Toggle(uint8_t Pin); // Flip the level of the TCA9554PWR Pin
/********************************************************* TCA9554PWR Initializes the device ***********************************************************/
void TCA9554PWR_Init(uint8_t PinState = 0x00); // Set the seven pins to PinState state, for example :PinState=0x23, 0010 0011 State (the highest bit is not used) (Output mode or input mode) 0= Output mode 1= Input mode. The default value is output mode
+157
View File
@@ -0,0 +1,157 @@
#include "Touch_CST820.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// I2C读写
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool I2C_Read_Touch(uint8_t Driver_addr, uint8_t Reg_addr, uint8_t *Reg_data, uint32_t Length)
{
Wire.beginTransmission(Driver_addr);
Wire.write(Reg_addr);
if ( Wire.endTransmission(true)){
printf("The I2C transmission fails. - I2C Read\r\n");
return -1;
}
Wire.requestFrom(Driver_addr, Length);
for (int i = 0; i < Length; i++) {
*Reg_data++ = Wire.read();
}
return 0;
}
bool I2C_Write_Touch(uint8_t Driver_addr, uint8_t Reg_addr, const uint8_t *Reg_data, uint32_t Length)
{
Wire.beginTransmission(Driver_addr);
Wire.write(Reg_addr);
for (int i = 0; i < Length; i++) {
Wire.write(*Reg_data++);
}
if ( Wire.endTransmission(true))
{
printf("The I2C transmission fails. - I2C Write\r\n");
return -1;
}
return 0;
}
struct CST820_Touch touch_data = {0};
uint8_t Touch_Init(void) {
pinMode(CST820_INT_PIN, INPUT_PULLUP);
CST820_Touch_Reset();
CST820_AutoSleep(false);
uint16_t Verification = CST820_Read_cfg();
// attachInterrupt(CST820_INT_PIN, Touch_CST820_ISR, interrupt);
return true;
}
/* Reset controller */
uint8_t CST820_Touch_Reset(void)
{
Set_EXIO(EXIO_PIN2,Low);
vTaskDelay(pdMS_TO_TICKS(10));
Set_EXIO(EXIO_PIN2,High);
vTaskDelay(pdMS_TO_TICKS(50));
return true;
}
uint16_t CST820_Read_cfg(void) {
uint8_t buf[3]={0};
I2C_Read_Touch(CST820_ADDR, CST820_REG_Version,buf, 1);
printf("TouchPad_Version:0x%02x\r\n", buf[0]);
I2C_Read_Touch(CST820_ADDR, CST820_REG_ChipID, buf, 3);
printf("ChipID:0x%02x ProjID:0x%02x FwVersion:0x%02x \r\n",buf[0], buf[1], buf[2]);
return true;
}
/*!
@brief Fall asleep automatically
*/
void CST820_AutoSleep(bool Sleep_State) {
CST820_Touch_Reset();
uint8_t Sleep_State_Set = 0;
if(Sleep_State)
Sleep_State_Set = 0;
else
Sleep_State_Set = 0xFF;
I2C_Write_Touch(CST820_ADDR, CST820_REG_DisAutoSleep, &Sleep_State_Set, 1);
}
// reads sensor and touches
// updates Touch Points
uint8_t Touch_Read_Data(void) {
uint8_t buf[6];
uint8_t touchpad_cnt = 0;
I2C_Read_Touch(CST820_ADDR, CST820_REG_GestureID, buf, 6);
/* touched gesture */
if (buf[0] != 0x00)
touch_data.gesture = (GESTURE)buf[0];
if (buf[1] != 0x00) {
noInterrupts();
/* Number of touched points */
touch_data.points = (uint8_t)buf[1];
if(touch_data.points > CST820_LCD_TOUCH_MAX_POINTS)
touch_data.points = CST820_LCD_TOUCH_MAX_POINTS;
/* Fill coordinates */
touch_data.x = ((buf[2] & 0x0F) << 8) + buf[3];
touch_data.y = ((buf[4] & 0x0F) << 8) + buf[5];
interrupts();
// printf(" points=%d \r\n",touch_data.points);
}
return true;
}
void example_touchpad_read(void){
Touch_Read_Data();
if (touch_data.gesture != NONE || touch_data.points != 0x00) {
// printf("Touch : X=%u Y=%u points=%d\r\n", touch_data.x , touch_data.y,touch_data.points);
} else {
// data->state = LV_INDEV_STATE_REL;
}
}
void Touch_Loop(void){
if(Touch_interrupts){
Touch_interrupts = false;
example_touchpad_read();
}
}
/*!
@brief handle interrupts
*/
uint8_t Touch_interrupts;
void IRAM_ATTR Touch_CST820_ISR(void) {
Touch_interrupts = true;
}
/*!
@brief get the gesture event name
*/
String Touch_GestureName(void) {
switch (touch_data.gesture) {
case NONE:
return "NONE";
break;
case SWIPE_DOWN:
return "SWIPE DOWN";
break;
case SWIPE_UP:
return "SWIPE UP";
break;
case SWIPE_LEFT:
return "SWIPE LEFT";
break;
case SWIPE_RIGHT:
return "SWIPE RIGHT";
break;
case SINGLE_CLICK:
return "SINGLE CLICK";
break;
case DOUBLE_CLICK:
return "DOUBLE CLICK";
break;
case LONG_PRESS:
return "LONG PRESS";
break;
default:
return "UNKNOWN";
break;
}
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "Arduino.h"
#include "I2C_Driver.h"
#include "TCA9554PWR.h"
#define CST820_ADDR 0x15
#define CST820_INT_PIN 16
#define CST820_LCD_TOUCH_MAX_POINTS (1)
/* CST820 GESTURE */
enum GESTURE {
NONE = 0x00,
SWIPE_UP = 0x01,
SWIPE_DOWN = 0x02,
SWIPE_LEFT = 0x03,
SWIPE_RIGHT = 0x04,
SINGLE_CLICK = 0x05,
DOUBLE_CLICK = 0x0B,
LONG_PRESS = 0x0C
};
//debug info
/****************HYN_REG_MUT_DEBUG_INFO_MODE address start***********/
#define CST820_REG_Mode 0x00
#define CST820_REG_GestureID 0x01
#define CST820_REG_Version 0x15
#define CST820_REG_ChipID 0xA7
#define CST820_REG_ProjID 0xA8
#define CST820_REG_FwVersion 0xA9
#define CST820_REG_AutoSleepTime 0xF9
#define CST820_REG_DisAutoSleep 0xFE
//Interrupt Modes
#define RISING 0x01
#define FALLING 0x02
#define CHANGE 0x03
#define ONLOW 0x04
#define ONHIGH 0x05
#define ONLOW_WE 0x0C
#define ONHIGH_WE 0x0D
#define interrupt RISING
extern uint8_t Touch_interrupts;
extern struct CST820_Touch touch_data;
struct CST820_Touch{
uint8_t points; // Number of touch points
GESTURE gesture; /*!< uint8_t */
uint16_t x; /*!< X coordinate */
uint16_t y; /*!< Y coordinate */
};
uint8_t Touch_Init();
uint8_t CST820_Touch_Reset(void);
void CST820_AutoSleep(bool Sleep_State);
uint16_t CST820_Read_cfg(void);
String Touch_GestureName(void);
uint8_t Touch_Read_Data(void);
void example_touchpad_read(void);
void IRAM_ATTR Touch_CST820_ISR(void);
+199
View File
@@ -0,0 +1,199 @@
#include "Wireless.h"
bool WIFI_Connection = 0;
uint8_t WIFI_NUM = 0;
uint8_t BLE_NUM = 0;
bool Scan_finish = 0;
uint8_t NUM=0;
uint8_t wifi_scan()
{
printf("/**********WiFi Test**********/\r\n");
// Set WiFi to station mode and disconnect from an AP if it was previously connected.
WiFi.mode(WIFI_STA);
WiFi.disconnect();
// WiFi.scanNetworks will return the number of networks found.
int n = WiFi.scanNetworks();
// Serial.println("Scan done");
if (n == 0)
{
printf("no networks found\r\n");
}
else
{
printf("Nr | SSID | RSSI | CH | Encryption\r\n");
for (int i = 0; i < 3; ++i)
{
// Print SSID and RSSI for each network found
printf("%2d | ", i + 1);
printf("%-32.32s | ", WiFi.SSID(i).c_str());
printf("%4d | ", WiFi.RSSI(i));
printf("%2d | ", WiFi.channel(i));
switch (WiFi.encryptionType(i))
{
case WIFI_AUTH_OPEN:
printf("open\r\n");
break;
case WIFI_AUTH_WEP:
printf("WEP\r\n");
break;
case WIFI_AUTH_WPA_PSK:
printf("WPA\r\n");
break;
case WIFI_AUTH_WPA2_PSK:
printf("WPA2\r\n");
break;
case WIFI_AUTH_WPA_WPA2_PSK:
printf("WPA+WPA2\r\n");
break;
case WIFI_AUTH_WPA2_ENTERPRISE:
printf("WPA2-EAP\r\n");
break;
case WIFI_AUTH_WPA3_PSK:
printf("WPA3\r\n");
break;
case WIFI_AUTH_WPA2_WPA3_PSK:
printf("WPA2+WPA3\r\n");
break;
case WIFI_AUTH_WAPI_PSK:
printf("WAPI\r\n");
break;
default:
printf("unknown\r\n");
}
vTaskDelay(10);
}
}
// Delete the scan result to free memory for code below.
WiFi.scanDelete();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
vTaskDelay(100);
printf("/*******WiFi Test Over********/\r\n\r\n");
printf("/**********BLE Test**********/\r\n");
}
BLEScan* pBLEScan;
uint8_t ble_scan()
{
if(NUM == 0){
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setActiveScan(true);
}
BLEScanResults* foundDevices = pBLEScan->start(5);
int count = foundDevices->getCount();
printf("Found %d devices\r\n",count);
for (int i = 0; i < count; i++) {
BLEAdvertisedDevice device = foundDevices->getDevice(i);
if (device.haveName()) {
printf("Bluetooth Name : %s\r\n",device.getName().c_str());
printf("Device address : %s\r\n",device.getAddress().toString().c_str());
printf("Response Length : %d\r\n",device.getPayloadLength());
printf("\r\n");
NUM++;
}
if(NUM == 2)
{
break;
}
}
if(NUM == 2)
{
pBLEScan->stop();
pBLEScan->clearResults();
BLEDevice::deinit(true);
printf("/**********BLE Test Over**********/\r\n\r\n");
}
return NUM;
}
void Wireless_Test1(){
uint8_t Time=0;
pinMode(0, INPUT);
while(digitalRead(0) != 0)
{
vTaskDelay(10);
Time++;
if(Time == 100)
{
printf("Please press the BOOT button \r\n");
Time=0;
}
}
wifi_scan();
uint8_t ble_NUM = ble_scan();
while(ble_NUM != 2)
ble_NUM = ble_scan();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int wifi_scan_number()
{
printf("/**********WiFi Test**********/\r\n");
// Set WiFi to station mode and disconnect from an AP if it was previously connected.
WiFi.mode(WIFI_STA);
WiFi.setSleep(true);
// WiFi.scanNetworks will return the number of networks found.
int count = WiFi.scanNetworks();
if (count == 0)
{
printf("No WIFI device was scanned\r\n");
}
else{
printf("Scanned %d Wi-Fi devices\r\n",count);
}
// Delete the scan result to free memory for code below.
WiFi.scanDelete();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
vTaskDelay(100);
printf("/*******WiFi Test Over********/\r\n\r\n");
return count;
}
int ble_scan_number()
{
printf("/**********BLE Test**********/\r\n");
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setActiveScan(true);
BLEScanResults* foundDevices = pBLEScan->start(5);
vTaskDelay(5000);
int count = foundDevices->getCount();
if (count == 0)
{
printf("No Bluetooth device was scanned\r\n");
}
else{
printf("Scanned %d Bluetooth devices\r\n",count);
}
pBLEScan->clearResults();
pBLEScan->stop();
BLEDevice::deinit(true);
vTaskDelay(100);
printf("/**********BLE Test Over**********/\r\n\r\n");
return count;
}
void WirelessScanTask(void *parameter) {
WIFI_NUM = wifi_scan_number();
BLE_NUM = ble_scan_number();
Scan_finish = 1;
vTaskDelete(NULL);
}
void Wireless_Test2(){
xTaskCreatePinnedToCore(
WirelessScanTask,
"WirelessScanTask",
4096,
NULL,
1,
NULL,
0
);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "WiFi.h"
#include <BLEDevice.h>
extern bool WIFI_Connection;
extern uint8_t WIFI_NUM;
extern uint8_t BLE_NUM;
extern bool Scan_finish;
uint8_t wifi_scan();
uint8_t ble_scan();
void Wireless_Test1();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int wifi_scan_number();
int ble_scan_number();
void Wireless_Test2();
+67
View File
@@ -0,0 +1,67 @@
#include "I2C_Driver.h"
#include "TCA9554PWR.h"
#include "Display_ST7701.h"
#include "LVGL_Driver.h"
#include "EyeScene.h"
#include "OtaService.h"
#include "../shared/GazeProtocol.h"
#ifndef EYE_BOARD_NUMBER
#define EYE_BOARD_NUMBER 1
#endif
namespace {
constexpr int kGazeRxPin = 44;
constexpr int kGazeBaud = 115200;
HardwareSerial gaze_serial(1);
gaze::Receiver gaze_receiver;
bool gaze_active = false;
uint32_t last_status_ms = 0;
}
void setup() {
I2C_Init();
TCA9554PWR_Init(0x00);
Set_EXIO(EXIO_PIN8, Low);
LCD_Init();
Lvgl_Init();
EyeScene_Create();
Serial.begin(115200);
gaze_serial.begin(kGazeBaud, SERIAL_8N1, kGazeRxPin, -1);
OtaService_Begin();
}
void loop() {
if (millis() - last_status_ms >= 5000) {
last_status_ms = millis();
if (Serial) Serial.printf("GAZE_READY board=%d recent=%d bad_format=%lu bad_sequence=%lu\n",
EYE_BOARD_NUMBER, gaze_receiver.recent(millis()),
static_cast<unsigned long>(gaze_receiver.rejected_format()),
static_cast<unsigned long>(gaze_receiver.rejected_sequence()));
}
gaze::Frame command = {};
while (gaze_serial.available()) {
const uint32_t old_format_rejections = gaze_receiver.rejected_format();
const uint32_t old_sequence_rejections = gaze_receiver.rejected_sequence();
if (gaze_receiver.feed(static_cast<uint8_t>(gaze_serial.read()),
millis(), &command)) {
EyeScene_SetGaze(command.x, command.y, command.valid);
gaze_active = command.valid;
if (Serial) Serial.printf("GAZE_ACCEPT board=%d seq=%u x=%u y=%u valid=%d\n",
EYE_BOARD_NUMBER, command.sequence, command.x,
command.y, command.valid);
}
if (gaze_receiver.rejected_format() != old_format_rejections)
if (Serial) Serial.printf("GAZE_REJECT board=%d reason=format\n", EYE_BOARD_NUMBER);
if (gaze_receiver.rejected_sequence() != old_sequence_rejections)
if (Serial) Serial.printf("GAZE_REJECT board=%d reason=sequence\n", EYE_BOARD_NUMBER);
}
if (gaze_active && !gaze_receiver.recent(millis())) {
EyeScene_SetGaze(500, 500, false);
gaze_active = false;
if (Serial) Serial.printf("GAZE_TIMEOUT board=%d\n", EYE_BOARD_NUMBER);
}
Lvgl_Loop();
OtaService_Loop();
vTaskDelay(pdMS_TO_TICKS(5));
}
+34
View File
@@ -0,0 +1,34 @@
# Local Wi-Fi and OTA updates
The two eye boards and the DFR1154 camera can receive application updates while on the same trusted **2.4 GHz home Wi-Fi** as the updating laptop. Eye gaze still travels over the existing UART wires; Wi-Fi is only for maintenance. Each device has its own identity: `eye-1`, `eye-2`, or `camera`. The update tool checks that identity and an embedded image marker before sending a binary. The device requires a one-use nonce and HMAC-SHA256 signature, checks the received SHA-256, writes the inactive app slot, and reboots. The update tool waits for the other slot to come online.
This is a local HTTP service on port 3232 with authenticated image uploads. **Do not port-forward it or expose it to the internet.** HMAC protects update authorization and integrity, but HTTP does not encrypt the image. Use only a trusted home network. The Wi-Fi password and OTA key are compiled into firmware, so keep binaries and the ignored `firmware/private/` directory private. The camera's bootloader has rollback enabled; the Arduino eye build does not have proven automatic rollback, so retain USB access for recovery.
## One-time setup
1. Run `python3 firmware/tools/configure_ota.py` from the project root and enter the 2.4 GHz Wi-Fi name and password at the prompts. It creates `firmware/private/ota.json` and `firmware/private/OtaSecrets.h` with an automatically generated update key. Both are ignored by Git. Do not share those files or paste the password into project documentation. For the 2026-09-30 deployment, Joshua supplied the credentials in `~/.llms/credentials.env`; only the Wi-Fi name and password were read to generate these private project files.
2. Clean-build both eye variants using the commands in [firmware/README.md](README.md). Board 1 and Board 2 must use their respective build paths and board-number setting. Keep the same 16 MB / 3 MB app partition scheme; it already contains `ota_0`, `ota_1`, and `otadata`.
3. Build [the camera IDF project](camera_person_idf/README.md) with ESP-IDF 5.5.2. Its partition table has two 7 MB app slots. For a camera still using the old single-slot layout, verify its hardware address, back up its current application, then perform a one-time USB flash that erases the old layout; `erase-flash` clears its entire 16 MB flash. Each eye likewise needs one USB flash of its matching Wi-Fi-enabled build. **The current three boards completed these USB steps on 2026-09-30; do not erase them again for ordinary application updates.**
4. Watch the USB logs for `OTA_READY device=… ip=… port=3232`. Reserve a stable IP for each board in the router, or record the current addresses. Check that eye motion, blinking, and wired gaze still work after joining Wi-Fi.
Without a local secrets header, builds remain possible but Wi-Fi/OTA is disabled and logs `OTA_DISABLED`. Test-only binaries built with a placeholder SSID are not deployment images; run the configuration tool and **clean-rebuild** before flashing real boards.
## Later updates
Build a fresh application image for the desired target. From the project root, update **one device at a time**:
```sh
python3 firmware/tools/push_ota.py --host EYE_1_IP --target eye-1 --bin firmware/build-eye-test/eye_test.ino.bin
python3 firmware/tools/push_ota.py --host EYE_2_IP --target eye-2 --bin firmware/build-eye-test-board2/eye_test.ino.bin
python3 firmware/tools/push_ota.py --host CAMERA_IP --target camera --bin firmware/camera_person_idf/build/freddy_camera_person.bin
```
The script rejects the wrong target marker, a device identity mismatch, or an image larger than its inactive slot. It reports success only after the device responds from the new slot. Confirm the expected eye behavior and camera detections after each update. If a device does not return, connect it by USB and inspect its log before attempting another update. OTA updates carry only the **application binary**; changes to the partition table or bootloader still require USB.
## Hardware verification — 2026-09-30
Each board was identified by its hardware address before USB flashing. Board 1 (`28:84:85:91:CF:F8`) and Board 2 (`28:84:85:91:D2:10`) were USB-flashed and verified with their distinct builds; USB logs showed both connected to Wi-Fi. The camera (`E8:F6:0A:87:0B:4C`) had its prior 3 MB application saved as the ignored `firmware/backups/camera-pre-ota-app-2026-09-30.bin` (whole-file SHA-256 `0e44f5f2bb80e59a1727046fee8351bb54713ca5b8673f173fc390ea34694b77`). Its new bootloader, dual-slot partition table, and application were USB-flashed and hash-verified.
The same matching application image was then sent to each device over Wi-Fi, one at a time. Each accepted its image, rebooted, and returned in **slot 1**. Current DHCP addresses at verification time were Board 1 `192.168.69.37`, Board 2 `192.168.69.141`, and camera `192.168.69.204`; these can change. A later `/ota/info` check confirmed all three identities and slot 1. The camera's live USB log still showed person-detection results and `gaze_valid` transitions; Board 2 continued accepting gaze packets, including valid targets. Board 1 initially showed no incoming gaze packets; Joshua fixed its cable, after which Board 1 accepted **48 gaze packets in 12 seconds**, including valid targets, with no rejections or timeouts in that sample. Post-update eye appearance has not yet been visually reconfirmed.
Wi-Fi reconnect after router loss and a power cycle of all three devices remain untested. On 2026-10-01, a later camera application update was installed by USB (the laptop was no longer on the devices' Wi-Fi subnet), leaving the camera in slot 0; both eyes remained on their 2026-10-01 OTA-updated slot 0. See [the tracking-filter check](../hardware/tracking-filter-2026-10-01.md).
+13
View File
@@ -0,0 +1,13 @@
# Pi-to-eye gaze link
`gaze_link.py` prepares the Raspberry Pi Zero 2 W to replace the DFRobot camera **as the one gaze-message sender**. It reproduces the ESP32 eye boards' [existing v1 packet](../../protocol/gaze.md), including CRC and sequence wrap; the eye firmware does not need reflashing. The Pi now boots Raspberry Pi OS Lite and detects an IMX708 camera, but the person detector and camera adapter remain to be implemented and benchmarked. This module has been tested on the laptop only; it has not sent a command from the Pi to the physical eyes.
On the Pi, enable the hardware UART and disable its serial login console. After the 2026-10-01 reboot, `/dev/serial0` exists, points to `/dev/ttyS0`, and the kernel command line no longer assigns a serial console. `open_pi_uart()` uses Python's standard library, so no `python3-serial` install is needed for the GPIO link. It sends normalized `x`, `y`, and `valid` values. Use `GazeLink` with an in-memory stream for development without driving the real eyes. The Pi GPIO14 TX/pin 8 goes to **both** eye GPIO44 RX pins and all three boards share ground. Disconnect the DFRobot camera TX first, and keep the Pi and ESP32 power rails separate. The eye UART USB-C connectors are a viable alternate path through a Pi USB hub, but they disable the eye UART headers while connected. See [Pi trial plan](../../planning/pi-vision.md).
Run the portable protocol check from this directory with `python3 -m unittest test_gaze_link.py`. It compares one encoded packet against a fixture generated by the existing C++ ESP32 protocol implementation. No camera or serial device is required for this check.
With the Pi UART enabled, the DFRobot TX wire disconnected, and both eye RX pins connected to Pi GPIO14, run `python3 smoke_test_link.py`. It sends left, center, and right gaze positions with the red tracking state for two seconds each, then returns both eyes to blue idle. On 2026-10-01 the script ran twice on the Pi, and Joshua visually confirmed both eyes matched throughout the second run.
For an initial **full-body detector benchmark**, install the Pi OS packages `python3-picamera2` and `python3-opencv`, then run `python3 benchmark_hog.py --duration 30 --rotation 180` with the current upside-down IMX708. It logs every frame's detection result and inference time under the project's `diagnostics/` directory. This OpenCV HOG detector is a baseline to measure; it requires most of a person's body in view and has not been accepted as the final person tracker. It does **not** send commands to the eyes. Judge its speed and false positives before connecting a detector to the gaze link. See the [Pi trial plan](../../planning/pi-vision.md).
The first integrated trial is `python3 run_hog_eye.py --duration 30 --rotation 180`. It logs what it **would** send while leaving the eyes untouched. Add `--drive-eyes` for a timed hardware trial. It confirms a person after two detections and holds the last gaze through short misses. The first moving-person run on 2026-10-01 had a longest raw-detection gap of 3.34 seconds and Joshua saw the eyes briefly return to blue. Replaying that log showed a 3.5-second hold bridges all three observed red-to-blue drops, so the next trial uses 3.5 seconds. This also delays the return to blue after a person leaves and does not move the gaze during a detection gap. At the end it sends blue idle. This is a one-person proof of concept; continuous target tracking, multiple-target continuity, day/night testing, and false-positive evaluation remain.
+104
View File
@@ -0,0 +1,104 @@
"""Measure a built-in OpenCV full-body detector on the Pi CSI camera.
This is an offline baseline: it logs detections but does not drive the eyes.
It will miss people whose full bodies are outside the frame.
"""
from __future__ import annotations
import argparse
import json
import statistics
import time
from datetime import datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--duration", type=float, default=30.0, help="seconds to sample")
parser.add_argument("--rotation", type=int, choices=(0, 180), default=0)
parser.add_argument("--width", type=int, default=320)
parser.add_argument("--height", type=int, default=240)
parser.add_argument("--log", type=Path, help="JSONL path (default: project diagnostics)")
args = parser.parse_args()
if args.duration <= 0 or args.width < 128 or args.height < 128:
parser.error("duration must be positive and frame dimensions at least 128 pixels")
import cv2
from libcamera import Transform
from picamera2 import Picamera2
cv2.setNumThreads(2)
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
transform = Transform(hflip=1, vflip=1) if args.rotation == 180 else Transform()
camera = Picamera2()
config = camera.create_video_configuration(
main={"size": (args.width, args.height), "format": "RGB888"},
transform=transform,
buffer_count=3,
)
camera.configure(config)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
log_path = args.log or PROJECT_ROOT / "diagnostics" / f"pi_hog_{stamp}.jsonl"
log_path.parent.mkdir(parents=True, exist_ok=True)
detection_ms: list[float] = []
detected_frames = 0
frames = 0
start = 0.0
try:
camera.start()
time.sleep(1.0) # Let exposure settle before measuring.
start = time.monotonic()
with log_path.open("w", encoding="utf-8") as log:
while time.monotonic() - start < args.duration:
frame = camera.capture_array("main")
detect_start = time.monotonic()
boxes, weights = hog.detectMultiScale(
frame,
hitThreshold=0.0,
winStride=(8, 8),
padding=(8, 8),
scale=1.05,
groupThreshold=1,
)
elapsed_ms = (time.monotonic() - detect_start) * 1000
detections = [
{"x": int(x), "y": int(y), "width": int(w), "height": int(h),
"score": float(score)}
for (x, y, w, h), score in zip(boxes, weights)
]
frames += 1
detected_frames += bool(detections)
detection_ms.append(elapsed_ms)
log.write(json.dumps({
"time": datetime.now().astimezone().isoformat(),
"frame": frames,
"detect_ms": round(elapsed_ms, 1),
"detections": detections,
}) + "\n")
if frames % 10 == 0:
print(f"frames={frames} detected={detected_frames} "
f"last_detect_ms={elapsed_ms:.0f}", flush=True)
finally:
camera.stop()
camera.close()
total = time.monotonic() - start
print(json.dumps({
"frames": frames,
"seconds": round(total, 2),
"fps": round(frames / total, 2) if total else 0,
"frames_with_person": detected_frames,
"median_detect_ms": round(statistics.median(detection_ms), 1) if detection_ms else None,
"log": str(log_path),
}, indent=2))
if __name__ == "__main__":
main()
+72
View File
@@ -0,0 +1,72 @@
"""Raspberry Pi sender for the two existing ESP32 eye boards' gaze protocol."""
from __future__ import annotations
import struct
from typing import BinaryIO
def crc16_ccitt(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def encode_frame(sequence: int, x: int, y: int, valid: bool) -> bytes:
"""Encode protocol v1; x and y are normalized camera coordinates (0..1000)."""
if not 0 <= sequence <= 0xFFFF or not 0 <= x <= 1000 or not 0 <= y <= 1000:
raise ValueError("sequence or coordinates out of range")
body = struct.pack("<2sBBHHH", b"\xA5\x5A", 1, int(bool(valid)), sequence, x, y)
return body + struct.pack("<H", crc16_ccitt(body))
def center_to_gaze(box: tuple[int, int, int, int], width: int, height: int) -> tuple[int, int]:
"""Convert a person box (left, top, right, bottom) to the eye protocol range."""
if width < 2 or height < 2:
raise ValueError("frame must be at least 2 by 2 pixels")
left, top, right, bottom = box
if not (0 <= left <= right < width and 0 <= top <= bottom < height):
raise ValueError("box must lie within frame")
return ((left + right) // 2 * 1000 // (width - 1),
(top + bottom) // 2 * 1000 // (height - 1))
class GazeLink:
"""Write one shared gaze stream; sequence advances even for invalid targets."""
def __init__(self, stream: BinaryIO) -> None:
self.stream = stream
self.sequence = 0
def send(self, x: int, y: int, valid: bool) -> int:
sequence = self.sequence
frame = encode_frame(sequence, x, y, valid)
written = self.stream.write(frame)
if written != len(frame):
raise OSError(f"short UART write: {written} of {len(frame)} bytes")
self.sequence = (sequence + 1) & 0xFFFF
return sequence
def open_pi_uart(device: str = "/dev/serial0") -> GazeLink:
"""Open the Pi's enabled hardware UART after disabling its login console."""
import os
import termios
fd = os.open(device, os.O_WRONLY | os.O_NOCTTY)
try:
settings = termios.tcgetattr(fd)
settings[0] = 0 # No input translation or software flow control.
settings[1] = 0 # No output translation.
settings[2] = termios.CLOCAL | termios.CREAD | termios.CS8 # 8N1, no RTS/CTS.
settings[3] = 0 # Raw mode; no terminal processing.
settings[4] = termios.B115200
settings[5] = termios.B115200
termios.tcsetattr(fd, termios.TCSANOW, settings)
return GazeLink(os.fdopen(fd, "wb", buffering=0))
except BaseException:
os.close(fd)
raise
+117
View File
@@ -0,0 +1,117 @@
"""Trial IMX708 full-body detection driving the two eyes over one Pi UART.
By default this only logs. Pass --drive-eyes for a timed hardware trial.
"""
from __future__ import annotations
import argparse
import json
import os
import time
from datetime import datetime
from pathlib import Path
from gaze_link import GazeLink, center_to_gaze, open_pi_uart
from tracking_state import TrackingState
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--duration", type=float, default=30.0)
parser.add_argument("--rotation", type=int, choices=(0, 180), default=0)
parser.add_argument("--drive-eyes", action="store_true")
parser.add_argument("--device", default="/dev/serial0")
parser.add_argument("--log", type=Path)
args = parser.parse_args()
if args.duration <= 0:
parser.error("duration must be positive")
import cv2
from libcamera import Transform
from picamera2 import Picamera2
cv2.setNumThreads(2)
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
camera = Picamera2()
transform = Transform(hflip=1, vflip=1) if args.rotation == 180 else Transform()
camera.configure(camera.create_video_configuration(
main={"size": (320, 240), "format": "RGB888"},
transform=transform,
buffer_count=3,
))
# The first moving-person log had a 3.34 s detector gap despite a visible person.
state = TrackingState(confirm_hits=2, hold_seconds=3.5)
link = open_pi_uart(args.device) if args.drive_eyes else GazeLink(open(os.devnull, "wb"))
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
log_path = args.log or PROJECT_ROOT / "diagnostics" / f"pi_eye_trial_{stamp}.jsonl"
log_path.parent.mkdir(parents=True, exist_ok=True)
started = False
frames = 0
tracked = 0
try:
camera.start()
started = True
time.sleep(1.0)
deadline = time.monotonic() + args.duration
with log_path.open("w", encoding="utf-8") as log:
while time.monotonic() < deadline:
frame = camera.capture_array("main")
detect_start = time.monotonic()
boxes, weights = hog.detectMultiScale(
frame, hitThreshold=0.0, winStride=(8, 8),
padding=(8, 8), scale=1.05, groupThreshold=1,
)
detect_ms = round((time.monotonic() - detect_start) * 1000, 1)
target = None
candidate = None
if len(boxes):
index = max(range(len(boxes)), key=lambda i: float(weights[i]))
box_x, box_y, width, height = (int(v) for v in boxes[index])
left = max(0, box_x)
top = max(0, box_y)
right = min(box_x + width - 1, frame.shape[1] - 1)
bottom = min(box_y + height - 1, frame.shape[0] - 1)
if left <= right and top <= bottom:
candidate = center_to_gaze(
(left, top, right, bottom), frame.shape[1], frame.shape[0]
)
target = {"x": box_x, "y": box_y, "width": width,
"height": height, "score": float(weights[index])}
x, y, valid = state.update(candidate, time.monotonic())
seq = link.send(x, y, valid)
frames += 1
tracked += valid
log.write(json.dumps({
"time": datetime.now().astimezone().isoformat(),
"frame": frames,
"detect_ms": detect_ms,
"target": target,
"gaze": {"x": x, "y": y, "valid": valid, "sequence": seq},
}) + "\n")
if frames % 10 == 0:
print(f"frames={frames} tracked={tracked} "
f"last_gaze={x},{y},{int(valid)}", flush=True)
finally:
# The eyes should return to blue whether the trial ends normally or fails.
try:
if args.drive_eyes:
for _ in range(3):
link.send(500, 500, False)
time.sleep(0.1)
finally:
link.stream.close()
if started:
camera.stop()
camera.close()
print(json.dumps({"frames": frames, "tracked_frames": tracked,
"log": str(log_path), "eyes_driven": args.drive_eyes}, indent=2))
if __name__ == "__main__":
main()
+42
View File
@@ -0,0 +1,42 @@
"""Briefly drive both eyes through the existing shared UART gaze link."""
from __future__ import annotations
import argparse
import time
from gaze_link import GazeLink, open_pi_uart
def send_state(link: GazeLink, x: int, y: int, valid: bool, duration: float) -> None:
deadline = time.monotonic() + duration
while time.monotonic() < deadline:
link.send(x, y, valid)
time.sleep(0.1) # The eye receiver returns to idle after 750 ms without a frame.
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--device", default="/dev/serial0", help="Pi GPIO UART device")
args = parser.parse_args()
link = open_pi_uart(args.device)
try:
for label, x, valid in (
("left/red", 200, True),
("center/red", 500, True),
("right/red", 800, True),
("idle/blue", 500, False),
):
print(label, flush=True)
send_state(link, x, 500, valid, 2.0)
finally:
# Leave the eyes blue even if the test is interrupted.
for _ in range(3):
link.send(500, 500, False)
time.sleep(0.1)
link.stream.close()
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
import io
import os
import pty
import select
import termios
import unittest
from gaze_link import GazeLink, center_to_gaze, crc16_ccitt, encode_frame, open_pi_uart
class GazeLinkTests(unittest.TestCase):
def test_crc_standard_vector_and_esp32_fixture(self):
self.assertEqual(crc16_ccitt(b"123456789"), 0x29B1)
# Generated by the current C++ gaze::encode used on the ESP32 boards.
self.assertEqual(
encode_frame(0x1234, 655, 774, True).hex(),
"a55a010134128f020603fa76",
)
def test_stream_and_sequence_wrap(self):
stream = io.BytesIO()
link = GazeLink(stream)
link.sequence = 0xFFFF
self.assertEqual(link.send(500, 500, False), 0xFFFF)
self.assertEqual(link.send(0, 1000, True), 0)
self.assertEqual(len(stream.getvalue()), 24)
self.assertEqual(stream.getvalue()[12:14], b"\xA5\x5A")
def test_box_center_and_invalid_inputs(self):
self.assertEqual(center_to_gaze((0, 0, 319, 239), 320, 240), (498, 497))
with self.assertRaises(ValueError):
center_to_gaze((0, 0, 320, 239), 320, 240)
with self.assertRaises(ValueError):
encode_frame(0, 1001, 0, True)
def test_serial_port_sends_115200_baud_frame(self):
master, slave = pty.openpty()
try:
link = open_pi_uart(os.ttyname(slave))
try:
settings = termios.tcgetattr(slave)
self.assertEqual(settings[4:6], [termios.B115200, termios.B115200])
link.send(200, 500, True)
self.assertTrue(select.select([master], [], [], 1)[0])
self.assertEqual(os.read(master, 12), encode_frame(0, 200, 500, True))
finally:
link.stream.close()
finally:
os.close(slave)
os.close(master)
if __name__ == "__main__":
unittest.main()
+39
View File
@@ -0,0 +1,39 @@
import unittest
from tracking_state import TrackingState
class TrackingStateTests(unittest.TestCase):
def test_two_hits_confirm_and_short_misses_hold(self):
state = TrackingState()
self.assertEqual(state.update((200, 400), 0.0), (500, 500, False))
self.assertEqual(state.update((250, 450), 0.2), (250, 450, True))
self.assertEqual(state.update(None, 1.8), (250, 450, True))
self.assertEqual(state.update((300, 450), 1.9), (300, 450, True))
self.assertEqual(state.update(None, 3.9), (500, 500, False))
def test_isolated_hit_does_not_trigger(self):
state = TrackingState()
self.assertFalse(state.update((100, 100), 0.0)[2])
self.assertFalse(state.update(None, 0.8)[2])
self.assertFalse(state.update((900, 900), 1.0)[2])
def test_recorded_moving_person_gap_is_held(self):
state = TrackingState(hold_seconds=3.5)
state.update((200, 600), 0.0)
self.assertTrue(state.update((210, 600), 0.2)[2])
self.assertTrue(state.update(None, 3.53)[2])
self.assertTrue(state.update((350, 600), 3.54)[2])
self.assertFalse(state.update(None, 7.04)[2])
def test_bad_configuration_and_coordinates(self):
with self.assertRaises(ValueError):
TrackingState(confirm_hits=0)
with self.assertRaises(ValueError):
TrackingState(hold_seconds=0)
with self.assertRaises(ValueError):
TrackingState().update((1001, 0), 0.0)
if __name__ == "__main__":
unittest.main()
+36
View File
@@ -0,0 +1,36 @@
"""Small Pi-side gate to keep both eyes on one person through brief misses."""
from __future__ import annotations
class TrackingState:
def __init__(self, confirm_hits: int = 2, hold_seconds: float = 2.0) -> None:
if confirm_hits < 1 or hold_seconds <= 0:
raise ValueError("confirmation and hold must be positive")
self.confirm_hits = confirm_hits
self.hold_seconds = hold_seconds
self.hits = 0
self.last_seen: float | None = None
self.valid = False
self.x = 500
self.y = 500
def update(self, candidate: tuple[int, int] | None, now: float) -> tuple[int, int, bool]:
if candidate is not None:
x, y = candidate
if not 0 <= x <= 1000 or not 0 <= y <= 1000:
raise ValueError("candidate gaze coordinates out of range")
if self.last_seen is None or now - self.last_seen > 0.75:
self.hits = 0
self.hits += 1
self.last_seen = now
if self.valid or self.hits >= self.confirm_hits:
self.valid = True
self.x, self.y = x, y
elif self.last_seen is not None:
if self.valid and now - self.last_seen >= self.hold_seconds:
self.valid = False
self.x = self.y = 500
if not self.valid and now - self.last_seen > 0.75:
self.hits = 0
return self.x, self.y, self.valid
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// A fixed-size, little-endian UART frame shared by the camera and both eyes.
namespace gaze {
constexpr uint8_t kMagic0 = 0xA5;
constexpr uint8_t kMagic1 = 0x5A;
constexpr uint8_t kVersion = 1;
constexpr uint8_t kValid = 0x01;
constexpr size_t kFrameSize = 12;
constexpr uint32_t kTimeoutMs = 750;
struct Frame {
uint16_t sequence;
uint16_t x; // 0..1000, left to right in the camera image
uint16_t y; // 0..1000, top to bottom in the camera image
bool valid;
};
inline uint16_t crc16(const uint8_t *bytes, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; ++i) {
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
for (int bit = 0; bit < 8; ++bit) {
crc = (crc & 0x8000) ? static_cast<uint16_t>((crc << 1) ^ 0x1021)
: static_cast<uint16_t>(crc << 1);
}
}
return crc;
}
inline void encode(const Frame &frame, uint8_t out[kFrameSize]) {
out[0] = kMagic0;
out[1] = kMagic1;
out[2] = kVersion;
out[3] = frame.valid ? kValid : 0;
out[4] = static_cast<uint8_t>(frame.sequence);
out[5] = static_cast<uint8_t>(frame.sequence >> 8);
out[6] = static_cast<uint8_t>(frame.x);
out[7] = static_cast<uint8_t>(frame.x >> 8);
out[8] = static_cast<uint8_t>(frame.y);
out[9] = static_cast<uint8_t>(frame.y >> 8);
const uint16_t crc = crc16(out, kFrameSize - 2);
out[10] = static_cast<uint8_t>(crc);
out[11] = static_cast<uint8_t>(crc >> 8);
}
inline bool decode(const uint8_t bytes[kFrameSize], Frame *out) {
if (bytes[0] != kMagic0 || bytes[1] != kMagic1 ||
bytes[2] != kVersion || (bytes[3] & ~kValid) != 0) return false;
const uint16_t actual_crc = static_cast<uint16_t>(bytes[10]) |
(static_cast<uint16_t>(bytes[11]) << 8);
if (crc16(bytes, kFrameSize - 2) != actual_crc) return false;
const uint16_t x = static_cast<uint16_t>(bytes[6]) |
(static_cast<uint16_t>(bytes[7]) << 8);
const uint16_t y = static_cast<uint16_t>(bytes[8]) |
(static_cast<uint16_t>(bytes[9]) << 8);
if (x > 1000 || y > 1000) return false;
out->sequence = static_cast<uint16_t>(bytes[4]) |
(static_cast<uint16_t>(bytes[5]) << 8);
out->x = x;
out->y = y;
out->valid = (bytes[3] & kValid) != 0;
return true;
}
class Receiver {
public:
// Returns true only for a complete, valid, fresh frame.
bool feed(uint8_t byte, uint32_t now_ms, Frame *out) {
if (length_ == 0 && byte != kMagic0) return false;
if (length_ == 1 && byte != kMagic1) {
length_ = byte == kMagic0 ? 1 : 0;
return false;
}
bytes_[length_++] = byte;
if (length_ < kFrameSize) return false;
Frame decoded = {};
const bool well_formed = decode(bytes_, &decoded);
if (well_formed) {
length_ = 0;
} else {
++rejected_format_;
// Keep an embedded preamble, or a trailing first magic byte.
size_t start = 1;
while (start < kFrameSize &&
(bytes_[start] != kMagic0 ||
(start + 1 < kFrameSize && bytes_[start + 1] != kMagic1))) {
++start;
}
length_ = kFrameSize - start;
for (size_t i = 0; i < length_; ++i) bytes_[i] = bytes_[start + i];
}
if (!well_formed) return false;
if (has_sequence_ && static_cast<uint32_t>(now_ms - last_ms_) <= kTimeoutMs) {
const uint16_t advance = static_cast<uint16_t>(decoded.sequence - last_sequence_);
if (advance == 0 || advance >= 0x8000) {
++rejected_sequence_;
return false;
}
}
has_sequence_ = true;
last_sequence_ = decoded.sequence;
last_ms_ = now_ms;
*out = decoded;
return true;
}
bool recent(uint32_t now_ms) const {
return has_sequence_ && static_cast<uint32_t>(now_ms - last_ms_) <= kTimeoutMs;
}
uint32_t rejected_format() const { return rejected_format_; }
uint32_t rejected_sequence() const { return rejected_sequence_; }
private:
uint8_t bytes_[kFrameSize] = {};
size_t length_ = 0;
bool has_sequence_ = false;
uint16_t last_sequence_ = 0;
uint32_t last_ms_ = 0;
uint32_t rejected_format_ = 0;
uint32_t rejected_sequence_ = 0;
};
} // namespace gaze
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include "mbedtls/md.h"
namespace freddy_ota {
inline bool is_hex(const char *value, size_t length) {
if (!value || strlen(value) != length) return false;
for (size_t i = 0; i < length; ++i) {
const char c = value[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false;
}
return true;
}
inline void to_hex(const uint8_t *bytes, size_t size, char *out) {
constexpr char digits[] = "0123456789abcdef";
for (size_t i = 0; i < size; ++i) {
out[2 * i] = digits[bytes[i] >> 4];
out[2 * i + 1] = digits[bytes[i] & 15];
}
out[2 * size] = 0;
}
inline bool constant_time_equal(const char *a, const char *b, size_t size) {
unsigned difference = 0;
for (size_t i = 0; i < size; ++i)
difference |= static_cast<unsigned>(a[i] ^ b[i]);
return difference == 0;
}
// Authentication binds one upload to its device, one-time nonce, image digest,
// and exact binary length. The key is never sent over the network.
inline bool verify(const char *key, const char *device, const char *nonce,
const char *sha256, size_t length, const char *signature) {
if (!key || strlen(key) < 32 || !is_hex(nonce, 32) ||
!is_hex(sha256, 64) || !is_hex(signature, 64)) return false;
char message[160];
const int n = snprintf(message, sizeof(message), "%s\n%s\n%s\n%zu",
device, nonce, sha256, length);
if (n < 0 || static_cast<size_t>(n) >= sizeof(message)) return false;
const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if (!md) return false;
uint8_t digest[32];
if (mbedtls_md_hmac(md, reinterpret_cast<const uint8_t *>(key), strlen(key),
reinterpret_cast<const uint8_t *>(message), n,
digest) != 0) return false;
char expected[65];
to_hex(digest, sizeof(digest), expected);
return constant_time_equal(expected, signature, 64);
}
} // namespace freddy_ota
+10
View File
@@ -0,0 +1,10 @@
#pragma once
// Generated locally by firmware/tools/configure_ota.py; never commit credentials.
#if __has_include("../private/OtaSecrets.h")
#include "../private/OtaSecrets.h"
#else
#define FREDDY_WIFI_SSID ""
#define FREDDY_WIFI_PASSWORD ""
#define FREDDY_OTA_KEY ""
#endif
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <stdint.h>
#include <stdlib.h>
// One camera-side decision is broadcast to both eyes so brief model misses do
// not make their appearance and gaze repeatedly switch between TRACK and IDLE.
namespace tracking {
constexpr unsigned kConfirmFrames = 3;
constexpr uint32_t kTargetHoldMs = 1000;
struct Decision {
int x;
int y;
bool valid;
bool held;
};
class Gate {
public:
Decision update(bool detected, int x, int y, uint32_t now_ms) {
if (detected) {
if (candidate_count_ && abs(x - candidate_x_) <= 80 &&
abs(y - candidate_y_) <= 60) {
if (candidate_count_ < kConfirmFrames) ++candidate_count_;
} else {
candidate_count_ = 1;
}
candidate_x_ = x;
candidate_y_ = y;
if (candidate_count_ >= kConfirmFrames) {
locked_ = true;
last_confirmed_ms_ = now_ms;
locked_x_ = x;
locked_y_ = y;
return {x, y, true, false};
}
}
if (locked_ && static_cast<uint32_t>(now_ms - last_confirmed_ms_) <=
kTargetHoldMs) {
return {locked_x_, locked_y_, true, true};
}
locked_ = false;
if (!detected) {
candidate_count_ = 0;
candidate_x_ = -1;
candidate_y_ = -1;
return {500, 500, false, false};
}
return {x, y, false, false};
}
private:
unsigned candidate_count_ = 0;
int candidate_x_ = -1;
int candidate_y_ = -1;
bool locked_ = false;
int locked_x_ = 500;
int locked_y_ = 500;
uint32_t last_confirmed_ms_ = 0;
};
} // namespace tracking
+64
View File
@@ -0,0 +1,64 @@
#include "GazeProtocol.h"
#include <assert.h>
#include <stdio.h>
namespace {
bool feed_frame(gaze::Receiver *receiver, const uint8_t *bytes,
uint32_t now_ms, gaze::Frame *out) {
bool accepted = false;
for (size_t i = 0; i < gaze::kFrameSize; ++i) {
accepted |= receiver->feed(bytes[i], now_ms, out);
}
return accepted;
}
} // namespace
int main() {
gaze::Frame sent = {65535, 0, 1000, true};
uint8_t bytes[gaze::kFrameSize];
gaze::encode(sent, bytes);
gaze::Frame received = {};
assert(gaze::decode(bytes, &received));
assert(received.sequence == sent.sequence && received.x == 0 &&
received.y == 1000 && received.valid);
gaze::Receiver receiver;
assert(feed_frame(&receiver, bytes, 100, &received));
assert(receiver.recent(850));
assert(!receiver.recent(851));
assert(!feed_frame(&receiver, bytes, 200, &received)); // duplicate
assert(receiver.rejected_sequence() == 1);
sent.sequence = 0; // wrap-around
sent.x = 500;
gaze::encode(sent, bytes);
assert(feed_frame(&receiver, bytes, 200, &received));
assert(received.sequence == 0 && received.x == 500);
sent.sequence = 65535; // out of order
gaze::encode(sent, bytes);
assert(!feed_frame(&receiver, bytes, 300, &received));
assert(receiver.rejected_sequence() == 2);
assert(feed_frame(&receiver, bytes, 1000, &received)); // reboot after timeout
bytes[2] = 2; // unsupported version
assert(!gaze::decode(bytes, &received));
gaze::encode(sent, bytes);
bytes[6] ^= 1; // bad checksum
assert(!feed_frame(&receiver, bytes, 1100, &received));
assert(receiver.rejected_format() == 1);
sent.sequence = 1;
sent.valid = false;
gaze::encode(sent, bytes);
receiver.feed(0xA5, 1100, &received); // truncated preamble
receiver.feed(0x00, 1100, &received);
assert(feed_frame(&receiver, bytes, 1100, &received));
assert(!received.valid);
sent.x = 1001;
gaze::encode(sent, bytes);
assert(!gaze::decode(bytes, &received));
puts("gaze protocol: round trip, CRC, version, range, resync, sequence, timeout OK");
}
+35
View File
@@ -0,0 +1,35 @@
#include "TrackingGate.h"
#include <assert.h>
#include <stdio.h>
int main() {
tracking::Gate gate;
assert(!gate.update(false, 0, 0, 0).valid);
assert(!gate.update(true, 160, 100, 250).valid);
assert(!gate.update(true, 165, 105, 500).valid);
const auto first_lock = gate.update(true, 170, 110, 750);
assert(first_lock.valid && !first_lock.held && first_lock.x == 170);
// Two missed model results keep both eyes tracking the last known position.
const auto missed = gate.update(false, 0, 0, 1000);
assert(missed.valid && missed.held && missed.x == 170 && missed.y == 110);
assert(gate.update(false, 0, 0, 1250).valid);
const auto reacquired = gate.update(true, 173, 108, 1500);
assert(reacquired.valid && !reacquired.held && reacquired.x == 173);
// A new distant candidate cannot yank the gaze until confirmed.
const auto other = gate.update(true, 290, 200, 1750);
assert(other.valid && other.held && other.x == 173);
assert(gate.update(true, 292, 201, 2000).held);
const auto switched = gate.update(true, 294, 202, 2250);
assert(switched.valid && !switched.held && switched.x == 294);
// Once the person leaves, the brief hold expires and blue/IDLE returns.
assert(gate.update(false, 0, 0, 3250).valid);
assert(!gate.update(false, 0, 0, 3251).valid);
assert(!gate.update(true, 160, 100, 3500).valid);
assert(!gate.update(true, 162, 102, 3750).valid);
assert(gate.update(true, 164, 104, 4000).valid);
puts("tracking gate: confirmation, brief misses, candidate switch, expiry OK");
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Print timestamped USB logs from the camera and both eye boards."""
import argparse
import select
import time
import serial
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--camera")
parser.add_argument("--eye1")
parser.add_argument("--eye2")
parser.add_argument("--duration", type=float, default=15)
args = parser.parse_args()
paths = {name: path for name, path in
(("camera", args.camera), ("eye1", args.eye1), ("eye2", args.eye2))
if path}
if not paths:
parser.error("provide at least one of --camera, --eye1, or --eye2")
ports = {name: serial.Serial(path, 115200, timeout=0)
for name, path in paths.items()}
buffers = {name: bytearray() for name in ports}
started = time.monotonic()
try:
while time.monotonic() - started < args.duration:
ready, _, _ = select.select(list(ports.values()), [], [], 0.2)
for port in ready:
name = next(name for name, candidate in ports.items() if candidate is port)
buffers[name].extend(port.read(port.in_waiting or 1))
while b"\n" in buffers[name]:
line, _, rest = buffers[name].partition(b"\n")
buffers[name] = bytearray(rest)
stamp = time.monotonic() - started
print(f"{stamp:8.3f} {name:6s} {line.decode(errors='replace').rstrip()}", flush=True)
finally:
for port in ports.values():
port.close()
if __name__ == "__main__":
main()
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Save local Wi-Fi credentials and a random OTA key outside version control."""
import getpass
import json
import secrets
from pathlib import Path
FIRMWARE = Path(__file__).resolve().parents[1]
PRIVATE = FIRMWARE / "private"
CONFIG = PRIVATE / "ota.json"
HEADER = PRIVATE / "OtaSecrets.h"
def main() -> None:
old = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
ssid = input(f"2.4 GHz Wi-Fi name [{old.get('ssid', '')}]: ").strip() or old.get("ssid", "")
if not ssid:
raise SystemExit("A Wi-Fi name is required")
password = getpass.getpass("Wi-Fi password (blank to keep saved value): ")
password = password or old.get("password", "")
if not password:
raise SystemExit("A Wi-Fi password is required")
key = old.get("ota_key") or secrets.token_hex(32)
PRIVATE.mkdir(mode=0o700, exist_ok=True)
CONFIG.write_text(json.dumps({"ssid": ssid, "password": password, "ota_key": key}, indent=2) + "\n")
CONFIG.chmod(0o600)
HEADER.write_text(
"#pragma once\n"
f"#define FREDDY_WIFI_SSID {json.dumps(ssid)}\n"
f"#define FREDDY_WIFI_PASSWORD {json.dumps(password)}\n"
f"#define FREDDY_OTA_KEY {json.dumps(key)}\n"
)
HEADER.chmod(0o600)
print("Saved local configuration in firmware/private/. Rebuild and flash each device once by USB.")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More