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
+20
View File
@@ -0,0 +1,20 @@
# Concealed camera placement
**2026-10-01 update:** Joshua now wants to try an Arducam OV5647 day/night camera with its two IR emitters in Freddy's mouth. See [the mouth placement trial](mouth-camera.md). The DFRobot chair-armrest pod described below remains a backup, not the selected location for the Pi trial.
The DFRobot DFR1154 V1.1 camera does not have to sit on Freddy's face or hat. It only needs a repeatable view of the front-door approach; its person position can be mapped to eye movement during calibration. Joshua rejected a front-of-hat badge because the lens and four onboard IR emitters would be obvious.
## Preferred placement to test
The mask will sit in a fake skeleton's lap on the **single wicker chair** in Joshua's [actual-chair photo](../reference_images/wicker_chair_skeleton.png), facing open concrete. Visitors approach from both sides. Try the [strap-on camera pod](../hardware/chair_camera_pod/README.md) beneath the **front half of the photo-right armrest**, with the lens and IR openings just below the front/outer woven lip. This is above the obscured seat underside and forward of the skeleton's hand. No mounting point is selected until live frames show both approach directions clearly and the pod remains inconspicuous from the walkway.
Do not put all five optical elements behind one close clear window: earlier IR testing washed out when the lens faced a nearby surface. A smoked or IR-pass cover would need measured transmission and opaque separation between the lens and emitters; it is not assumed to work. Likewise, a deep tunnel in front of the 160° lens would narrow its view. A concealed position is preferable to trying to camouflage a visible front-facing box.
## Position test before CAD is finalized
1. Hold the camera just beneath the photo-right armrest's front half using a temporary non-damaging support. Aim it across the part of the path where visitors approach the door from either side. Keep the lens and IR emitters clear of the wicker edge and the skeleton's hand.
2. Save a frame with a person at the far, middle, and near points, and one empty frame. The head and upper body should remain in view. Check that the armrest, skeleton, mask, and door frame do not block the lower or side frame.
3. Repeat at night with onboard IR. From the walking path, check that the camera is not conspicuous, the IR image is usable, and nearby surfaces do not reflect into the lens.
4. Measure the usable underside opening, strap route, desired camera angle, cable route, and nearest source of power. Adjust the pod or its mounting angle to those measurements before the final print.
The eventual housing should have a removable back or board tray, wire strain relief and a drip loop, dark non-reflective surfaces around the lens, and mounting that permits aiming after installation. It still needs protection from wind-driven rain and condensation even under the porch roof.
+12
View File
@@ -0,0 +1,12 @@
# Freddy mouth camera placement trial
Joshua wants to try the Arducam OV5647 day/night camera with two IR LED boards inside Freddy's mouth. The goal is to make the hardware read as part of the character while the mask rests in the seated skeleton's lap. This is a placement trial, not yet a verified fit or sightline. The previous chair-armrest DFRobot pod is a fallback; its geometry is not reusable without redesign.
The [Arducam B003503-type module](https://www.arducam.com/arducam-m12-night-vision-ir-cut-raspberry-pi-camera.html) uses a 36 × 36 mm camera board, two separate IR LED boards, and an automatically switched IR-cut filter. Its lens is about 105° horizontally. The IR lighting and filter respond to ambient light rather than person detection, so their visible appearance will not automatically pulse with the red-eye tracking state. The full three-board assembly, heat sinks, flex bend, mouth opening, teeth, and muzzle clearances must be measured from the actual parts and mask. Do not cut the mask or design a final housing from the product photo alone.
1. Hold the unpowered camera assembly in the mouth at the mask's intended seated angle. Check the view from visitors approaching left, center, and right; avoid letting the upper muzzle or teeth block the lens or LEDs.
2. Boot the Pi and capture day and night frames from that temporary position. Compare with the DFRobot's view, checking person size, usable horizontal coverage, focus, and IR reflections from the mouth interior. Keep each emitter optically separated from the lens, especially if a front cover is used.
3. View the unlit and IR-lit mouth from the walkway. The IR emitters may show only faint red points rather than an area glow; their exact wavelength is not established. If a visible eerie aura is wanted, prototype a separate low-brightness visible red light, visually diffused around the mouth and shielded from the camera lens. Its on/off timing can later follow the same valid-person state as the eyes.
4. Only after the images and appearance work, measure a removable mount, cable exit, LED heat-sink clearance, rain shielding, and service access. Retain a clear, non-reflective optical path; do not put lens and emitters behind one close, shared clear window.
The Pi still sends one gaze packet stream to both ESP32 eyes. Mouth position changes camera-to-eye calibration, so tune gaze mapping after the mount angle is fixed. Person-detector speed and reliability on the Pi remain unmeasured.
+24
View File
@@ -0,0 +1,24 @@
# Outdoor camera deployment
**2026-10-01 update:** For the proposed Pi trial, Joshua prefers to test the Arducam day/night camera in Freddy's mouth, using its paired IR emitters as part of the character effect. See [mouth placement](mouth-camera.md). The DFRobot chair-armrest placement below remains the current hardware's fallback plan. The mouth trial needs its own lens/IR reflection, sightline, and weather checks.
The Freddy mask is intended as a stationary prop on a covered porch beside the front door, following people who walk up to the house. The DFRobot DFR1154 V1.1 is a plausible camera for this close, sheltered position, but reliability there is not yet established. Indoor held-position tests produced coordinates at approximately four updates per second with a typical 233 ms capture-to-result time. Empty-room tests also produced false detections at the current score threshold.
DFRobot specifies an OV3660 camera with a 160° field of view, visible/940 nm IR sensitivity, onboard IR illumination, and an operating temperature of -10 to 60 °C. Its [product documentation](https://wiki.dfrobot.com/dfr1154/) does not state a water or dust protection rating. The porch roof helps, but the bare board, USB connector, and camera flex still need protection from wind-driven rain, condensation, and insects. A mask cavity alone is not assumed to be sealed.
## Camera placement
- The mask will sit in a skeleton's lap on the single wicker chair in the [actual-chair photo](../reference_images/wicker_chair_skeleton.png). The camera and its four IR emitters must be hidden from visitors' normal view; an exposed hat badge was rejected. The [armrest underside pod](../hardware/chair_camera_pod/README.md) is a first-fit candidate beneath the front half of the photo-right armrest. The lens and IR openings must sit below the front/outer wicker edge, clear of the skeleton's hand, with useful person-detection frames from both approach directions. A separate camera viewpoint can still supply coordinates to the eyes after calibration.
- Aim the camera at the front-door approach rather than the whole yard. The very wide field of view makes distant people small in the 320 × 240 frames used by the current detector, but visitors near the door should be much larger. Measure the actual approach distance and inspect saved frames before fixing the mount.
- Give the lens an unobstructed view. Keep any protective window clean and separated from the onboard IR LEDs, or place IR illumination outside that window. A close surface already caused an IR-lit purple washout during bench testing.
- Use the porch roof as shade while preserving a view of the approach. Check for strong backlight from the yard and reflections from the front door or sidelights; indoor success does not predict performance under those conditions.
## Outdoor acceptance test
1. Place the camera at the proposed chair-armrest height and angle with its intended housing. Record the path distance and a frame with a person at the near, middle, and far parts of the intended tracking zone.
2. Record repeated normal-speed walk-bys in daylight, deep shade, dusk, and darkness. Include approach from both directions and a clear path. Measure detection rate, missed runs, false triggers, and capture-to-coordinate latency from saved logs and images.
3. Verify whether the onboard IR lights improve the night walk-by results at the installed distance. Check the window for IR glare and the camera for condensation after a temperature change.
4. Test two people crossing and short occlusion. A stable target should continue through a brief miss and clear when the person leaves. The current raw model output is not yet suitable to command the eyes directly because of observed false positives.
5. Run the complete protected installation for at least 30 minutes with its final power supply. Confirm stable camera and eye behavior, clear lens, dry connections, and acceptable internal temperature.
If people remain too small or tracking is too slow at the real path distance, adjust camera aim and resolution/model first, then consider a narrower-field camera or different detector. Keep the installed sensor choice contingent on these tests.
+64
View File
@@ -0,0 +1,64 @@
# Pi Zero 2 W vision-controller trial
Joshua wants to keep both Waveshare ESP32-S3 eye displays and trial his Raspberry Pi Zero 2 W for person detection, target selection, synchronization, and diagnostic logs. He identified an [InnoMaker CAM-OV5647 with an M12 90° diagonal IR-filter lens](https://www.inno-maker.com/product/cam-ov5647/) and showed two Arducam alternatives; his current placement preference is the Arducam day/night camera in Freddy's mouth. This is the preferred **trial architecture**, not yet a measured replacement for the DFRobot camera. The Pi now boots Raspberry Pi OS Lite and is reachable by SSH as `overseer@10.60.1.215`. On 2026-10-01, `rpicam-hello --list-cameras` detected an **IMX708 Camera Module 3** at CSI0; this is not either of the Arducam modules shown earlier. The IMX708 sensor identification does not establish whether the module is the standard or NoIR variant. Physical availability of the Arducam board is unconfirmed. The portable [Pi gaze sender](../firmware/pi_vision/README.md) encodes the existing ESP32 packet and passes local tests, but has not sent a command from the Pi to either eye. Keep the working DFRobot setup available until a Pi walk-by test meets the same gaze-link behavior.
## Hardware split
```text
Pi CSI camera → Pi Zero 2 W (detection, one target, tracking state, logs)
GPIO14 TX, 3.3 V UART, shared ground
├── eye 1 GPIO44 RX → local eye animation
└── eye 2 GPIO44 RX → local eye animation
```
The Pi sends the existing [12-byte gaze frame](../protocol/gaze.md) at 115200 baud to both eyes. `valid=1` already means the eyes track and glow red; `valid=0` means idle and blue. Their firmware, Wi-Fi OTA path, iris art, blink, and display wiring do not need changing for the first Pi trial. Disconnect the DFRobot camera's TX from the shared UART line before connecting Pi TX; two transmitter outputs must not be tied together. Join the Pi and both eye grounds, but do not join 3.3 V output rails. The Pi's primary UART is on GPIO14/pin 8; enable UART hardware and disable the serial login console. On Zero 2 W the default primary UART is a mini UART, so consider Raspberry Pi's `disable-bt` overlay if its timing proves unreliable at 115200 baud. [Raspberry Pi UART documentation](https://www.raspberrypi.com/documentation/computers/configuration.html).
Both eye boards also have a USB-to-UART Type-C port. A Pi USB OTG host adapter and hub could connect both as separate serial devices and send the same gaze packet to each **without reflashing the eye firmware**. That port's connection disables the eye's 4-pin UART header, so do not use the two paths simultaneously. USB needs a hub, two serial-port writes and stable device identification; the GPIO fan-out is the simpler final arrangement and delivers the same packet to both eyes on one wire. The separate Type-C port labeled `USB` remains available for eye power, diagnostics, and flashing while the 4-pin UART header is used. [Waveshare interface map](https://docs.waveshare.com/ESP32-S3-Touch-LCD-2.1).
Pi serial state after Joshua ran `raspi-config` and rebooted on 2026-10-01: `/dev/serial0` points to `/dev/ttyS0`; `enable_uart=1` is set; the kernel command line has only `console=tty1`; GPIO14 is in TXD1 mode. The `overseer` account belongs to `dialout`. The Pi sender now uses Python's built-in `termios`, so it needs no serial package. The physical Pi-to-eye connection and packet delivery still need verification.
Camera check on 2026-10-01: `rpicam-hello --list-cameras` initially found the IMX708 and a 640 × 480 `rpicam-still` capture completed. The first frame is saved at [`../diagnostics/pi_imx708_first_frame.jpg`](../diagnostics/pi_imx708_first_frame.jpg), but the lens was blocked. After the UART-config reboot, automatic camera detection failed, even after the ribbon was checked and the Pi rebooted again. `vcgencmd get_throttled` returned `0x0`; the boot device tree had no `imx708` node. Joshua ran `sudo dtoverlay imx708` as a **temporary runtime diagnostic**, after which `rpicam-hello --list-cameras` immediately found the sensor and still capture worked. This points to failed automatic overlay loading at boot, not a disconnected sensor. The overlay will not persist across a reboot; tomorrow's different IR camera may need a different sensor overlay.
The latest useful stills are [`../diagnostics/pi_imx708_rotated_view.jpg`](../diagnostics/pi_imx708_rotated_view.jpg) and [`../diagnostics/pi_imx708_person_view.jpg`](../diagnostics/pi_imx708_person_view.jpg). The module currently produces a 180°-inverted frame, corrected by `--rotation 180` in `rpicam-still` or `Transform(hflip=1, vflip=1)` in Picamera2. The second image shows a full standing person in the indoor view. `python3-picamera2` and `python3-opencv` were installed on the Pi for the benchmark.
The Pi-to-eye GPIO link was exercised twice with [`smoke_test_link.py`](../firmware/pi_vision/smoke_test_link.py). The second run was visually confirmed by Joshua: both eyes moved left, center, right together while red, then returned to blue idle. This establishes the Pi UART wiring and packet compatibility but does not yet prove camera-driven tracking.
## 2026-10-01 IMX708 person-tracking trial
- At 320 × 240 with OpenCV's built-in full-body HOG detector, a 15-second empty-room sample processed **94 frames at 6.09 fps** with zero detections. A 20-second stationary-person sample processed **126 frames at 6.16 fps**, detecting a person in 106 frames. Median detector time was about 156 ms. Logs: [`../diagnostics/pi_hog_20261001-200202.jsonl`](../diagnostics/pi_hog_20261001-200202.jsonl) and [`../diagnostics/pi_hog_20261001-200343.jsonl`](../diagnostics/pi_hog_20261001-200343.jsonl).
- A 25-second moving-person live trial initially detected a person in only 43/157 raw frames. The longest raw gap was 3.34 seconds; a 2-second hold allowed three red-to-blue drops, and Joshua saw flicker. The gaze link kept both eyes synchronized. Log: [`../diagnostics/pi_eye_trial_20261001-201006.jsonl`](../diagnostics/pi_eye_trial_20261001-201006.jsonl).
- Replaying that log with a 3.5-second hold eliminated its three intermediate red-to-blue drops. A new 25-second live trial processed **158 frames**, detected a person in 92 raw frames, and held valid tracking for 140 frames. Gaze X ranged 109–605/1000, with **zero intermediate red-to-blue drops**. Joshua visually confirmed both eyes stayed red and followed together. Log: [`../diagnostics/pi_eye_trial_20261001-201350.jsonl`](../diagnostics/pi_eye_trial_20261001-201350.jsonl).
- This establishes camera-to-Pi-to-both-eyes integration indoors. The HOG detector still misses moving people for seconds at a time; during a held miss, eye gaze freezes at the last position, and after departure red can linger for up to 3.5 seconds. IMX708 detection at boot has been intermittent, and multiple-person selection is naive. The porch night result is recorded below. The Arducam IR camera swap requires a new sensor identification and may need its own overlay before repeating the benchmarks.
## Porch relocation check — 2026-10-01 night
- After a clean shutdown and move, the Pi rejoined Wi-Fi and `/dev/serial0` remained available. On this reboot the IMX708 **was** auto-detected without a manual overlay, so the earlier intermittent boot-detection failure is unresolved rather than permanent. The porch-mounted Pi-to-eye smoke test again made both displays look left, center, and right together in red, then return to blue; Joshua confirmed it visually.
- The camera still needs 180° software rotation. The unlit upright frame [`../diagnostics/pi_imx708_porch_upright.jpg`](../diagnostics/pi_imx708_porch_upright.jpg) shows the railing/posts but almost no usable detail in the visitor path. With the porch light on, [`../diagnostics/pi_imx708_porch_lit.jpg`](../diagnostics/pi_imx708_porch_lit.jpg) shows Joshua, though the image remains dim and his lower body is cut off.
- A 12-second **detector-only** lit-porch sample processed 76 frames at 6.05 fps and detected a person in just **2 frames**, while Joshua confirmed he was visible most of the time. Evidence: [`../diagnostics/pi_hog_20261001-202712.jsonl`](../diagnostics/pi_hog_20261001-202712.jsonl). **No-go for using this IMX708/HOG combination as a dependable night visitor trigger in the current porch view.** Do not leave `run_hog_eye.py --drive-eyes` running unattended on this evidence. The Arducam IR camera arriving 2026-10-02 should be evaluated with its actual night image and full-body framing; better illumination alone may not fix the HOG detector's failure on partial bodies.
The [InnoMaker listing](https://www.amazon.com/dp/B07G9VLPZH) says this OV5647 module includes both standard and Pi Zero camera ribbons, though the actual cables in Joshua's kit should be checked. Its 39 × 39 mm board and manual-focus M12 lens need a new physical fit check; the existing DFRobot chair pod is not assumed to fit. Raspberry Pi's camera software supports the OV5647 sensor, but the image still needs verification on this third-party board. [Raspberry Pi camera software](https://www.raspberrypi.com/documentation/computers/camera_software.html).
The identified lens has an IR filter, so do not assume a separate IR illuminator will provide useful night vision. Test with the real porch lighting. If the night image is inadequate, check whether Joshua owns another NoIR camera or a confirmed compatible no-IR-filter lens before changing hardware. The camera and lighting still need the concealed chair placement and weather protection described in [outdoor deployment](outdoor-deployment.md).
## Additional camera candidates identified 2026-10-01
Joshua showed an [Arducam 8 MP IMX219 NoIR](https://www.arducam.com/8mp-imx219-noir-for-raspberry-pi-b0395.html) and an [Arducam 5 MP OV5647 day/night camera with switchable IR-cut filter and two IR LED boards](https://www.arducam.com/arducam-for-raspberry-pi-noir-5mp-ov5647-camera-module-motorized-ir-cut-filter-for-daylight-and-night-vision-support-pi-4-zero-pi-3.html). Do not assume these are physically in hand until Joshua confirms. Both list Zero 2 W support and Pi Zero camera cables. The NoIR's 25 × 24 mm board is easier to conceal, but its fixed lens covers about 62° horizontally and needs separate IR illumination. The switchable OV5647 covers about 105° horizontally, offers daytime color and night IR, and has a 36 × 36 mm camera board plus two adjacent IR LED boards. Camera capture rate does not establish Pi person-detection rate; benchmark the detector separately.
Joshua's preferred first placement trial for the switchable OV5647 is **inside Freddy's mouth**, not behind the eyes or under the chair armrest. In this character location the two IR emitters may contribute a faint visible red point-glow, but their wavelength and appearance are unverified; do not count on them to produce a broad red aura. A separate dim visible LED effect could supply that look if desired. The mouth cavity must leave lens and both emitters with a clear forward sightline, separate them from reflective teeth/muzzle surfaces, and provide cable/heat clearance. See [mouth camera placement](mouth-camera.md). The earlier chair-armrest DFRobot pod remains a backup and does not fit the Arducam by assumption.
## Trial sequence
1. Confirm which camera is physically available, check its 15-to-22-pin Pi Zero ribbon, and boot the Pi with Raspberry Pi OS and that camera attached. Verify it appears in `rpicam-hello --list-cameras`. Save a daylight frame and, if using the InnoMaker or switchable OV5647, set manual focus for the visitor path. Check a night frame with the actual porch lighting before choosing an IR solution.
2. Benchmark a lightweight person detector on the Zero 2 W using the actual camera view. This Pi has a quad-core 1 GHz CPU and 512 MB RAM, so person-detection speed and memory must be measured before assuming it improves on the roughly four-results-per-second ESP32 baseline. Record frames processed per second, detection latency, CPU/memory use, and example frames with and without people. [Pi Zero 2 W specifications](https://www.raspberrypi.com/news/new-raspberry-pi-zero-2-w-2/).
3. Add one-target continuity and a short miss hold, then translate the chosen bounding-box center into the existing 0–1000 gaze coordinates. Emit exactly one serial stream to both eyes. Log timestamp, detector confidence and box, selected target, gaze validity and coordinates, and transmitted sequence locally with rotation; video need not be recorded continuously.
4. Disconnect DFRobot TX, connect Pi TX and common ground, and confirm both eyes accept the same frame sequences. Compare their visible gaze direction and red/blue state during approach, departure, an empty scene, and two-person crossings.
5. Evaluate from the actual concealed chair position in daylight, dusk, and darkness before retiring the DFRobot camera. A Pi-side change cannot by itself synchronize independent eye blink or glow-pulse phase; if those effects need frame-accurate synchronization, extend the gaze protocol with a shared animation cue in a later step.
If CPU-only inference cannot meet the measured response target, a Pi AI Camera is a potential later upgrade: Raspberry Pi documents its IMX500 on-camera object detection and says it can be used with Zero 2 W with minor setup changes. This is a fallback, not a purchase recommendation before benchmarking the camera Joshua already owns. [Raspberry Pi AI Camera documentation](https://www.raspberrypi.com/documentation/accessories/ai-camera.html).
## Acceptance gate
- Both eyes receive the same valid/invalid state and coordinates from the Pi with no dropped or malformed frames in a sustained bench run.
- A visitor walking across the intended porch path stays selected without rapid switching; departures return both eyes to blue promptly.
- Empty-path tests do not trigger persistent red eyes. Measure false triggers across day, dusk, and night instead of inferring performance from indoor samples.
- The Pi meets or improves on the current ~4 Hz camera result rate and keeps end-to-end visible response acceptable in the installed view; otherwise retain the ESP32 camera or try an accelerator.
+37
View File
@@ -0,0 +1,37 @@
# Red-eye mode on confirmed tracking
Status: tracking-triggered eye firmware deployed to both boards over Wi-Fi on 2026-10-01; visual confirmation pending. The earlier two-foot distance proposal below is superseded.
## Current behavior
The camera sends `valid=1` only after three consistent person detections. Both eyes use that existing signal to enter the dark, glowing-red appearance while they show `TRACK`; `valid=0` or 750 ms without an accepted gaze frame restores the normal blue appearance and `IDLE`. Gaze motion and blinking continue. No separate range sensor, wiring, or camera-protocol change is needed. Screen taps no longer toggle the appearance. Test both eyes with a person entering and leaving the camera view, then test the empty porch: a persistent false person detection can still turn the eyes red.
## Superseded two-foot proposal
## Intended behavior
When a person comes within about two feet (61 cm) of the mask, both eyes switch from the normal blue iris to a dark eye with a small glowing red center, based on [`freddy_glowing_red_pupils.png`](../reference_images/freddy_glowing_red_pupils.png). Gaze motion and blinking continue in either state. The reference image is a visual target; its contents are not project instructions. The normal blue look also has a [visual reference](../reference_images/freddy_glowing_blue_eyes.png).
The current [red preview](../firmware/assets/freddy_eye_red_preview.png) uses a dark sclera, a dark moving iris, and layered red-to-white circles for the lit center. A screen tap toggles the visual state independently on each eye for bench review. The public `EyeScene_SetProximityRed(bool)` hook is ready for a later camera message handler. No measured-distance input or synchronization is connected yet.
## Sensing decision to validate
The existing [DFRobot DFR1154 camera](https://wiki.dfrobot.com/dfr1154/) has a single OV3660 image sensor and no listed depth/ranging sensor. Apparent person size could estimate distance after calibration, but person height, posture, and the 160° wide-angle view make it a poor basis for a repeatable 61 cm boundary. The leading approach is to pair person localization with a range sensor on the camera/controller side.
Joshua's order record identifies four owned choices: an HC-SR04 ultrasonic sensor, a Benewake TF-Luna, a TFmini Plus, and a TFmini-S. **Bench the TFmini Plus first; no new sensor purchase is needed for the initial test.** [Benewake's TFmini Plus specification](https://en.benewake.com/TFminiPlus/index.html) gives a 0.1–12 m range, ±5 cm accuracy from 0.1–5 m, and a 3.6° field of view. Its 10 cm minimum is preferable to the [TF-Luna's](https://en.benewake.com/uploadfiles/2025/04/20250430174515390.pdf) 20 cm minimum when someone is very close to the mask. Both cover the 61 cm trigger. The Plus's beam is still narrow: it measures a small target area and cannot identify whether the return came from the tracked person.
A [receive-only DFR1154 bench sketch](../firmware/benewake_range_bench/README.md) compiles and is ready for wiring verification. It has not been flashed or tested with a physical sensor.
For wiring, the [TFmini Plus manual](https://en.benewake.com/uploadfiles/2025/04/20250430175221028.pdf) specifies 5 V ±0.5 V power, 140 mA peak current, and **3.3 V UART signaling** at 115200 baud by default. Use a regulated supply and common ground; do not power it from a 3.3 V signal pin. The camera board's available power rail, board identity, and exact UART connector pinout must be verified before connecting it. Confirm the actual sensor cable against the manual before applying power; the order screenshot identifies the model, not the individual wire colors on the unit at hand.
The [TFmini-S](https://en.benewake.com/TFminiS/index.html) also starts at 10 cm and is lighter; it remains a useful mounting comparison. The [HC-SR04 datasheet](https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf) specifies 2–400 cm range and a roughly 15° measuring angle. It is a useful wider-beam comparison, but its 5 V Echo signal needs level shifting or a voltage divider before an ESP32 input. All four sensors detect objects, so pair a valid range with camera-confirmed person presence and test false triggers from nearby props.
The previously considered [VL53L1X](https://www.st.com/en/imaging-and-photonics-solutions/vl53l1X) and [VL53L5CX](https://www.st.com/resource/en/datasheet/vl53l5cx.pdf) remain future options only if the owned sensors cannot cover the desired approach area.
Proposed state logic for bench testing: enter red after a valid person and matched range at or below 610 mm for 200 ms; return to normal when range exceeds 760 mm for 300 ms or the person/range signal becomes stale. Hysteresis prevents rapid color flicker near the boundary. These timings are starting values, not measured results. Both eye boards should receive the same state in the versioned camera message, then fall back to normal idle animation if messages stop.
## Acceptance checks
- Test TFmini Plus readings at 10, 20, 45, 60, and 75 cm, plus a very-close approach, off-center positions, and a nearby non-person object; record range errors and false triggers. Compare TF-Luna, TFmini-S, or HC-SR04 if the narrow beam or mounting position causes missed triggers.
- Show both eyes enter and leave red together, with gaze and blinks uninterrupted.
- Show sensor/camera disconnection returns both eyes to normal without a reboot.