pyronear / pyronear/pyro-engine
Heartbeat keeps reporting cameras as alive when they can be actually down (stale cached frame)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 19
- Forks
- 8
- PR merge metrics
- No merged PRs in 30d
Description
Pitch
The engine sends a device-liveness heartbeat to the PyroNear API for a camera as long as the camera API returns any frame for it BUT that frame comes from a cache that is never invalidated. Once a camera has produced one image, it will keep heart-beating "alive" to the cloud indefinitely even after the physical camera goes offline. Operators watching camera health on the platform can't trust the heartbeat to tell them a camera is actually reachable.
Why it's a problem
The heartbeat is gated on frame is not None, and the frame is a cached image, not a live capture:
- SystemController.inference_loop fetches a frame, and only calls engine.predict(...) if it's non-None — core.py:201-205.
- predict is the only place the heartbeat fires — engine.py:268-269 → engine.py:222-225 (api_client[ip].heartbeat()).
- The frame is read from cam.last_images — routes_cameras.py:254-256 — which is only written on a successful capture and never cleared on failure: the patrol loop logs and continues, leaving the old frame in place — patrol.py:89-96.
- There is no TTL and no clear-on-failure. The only place the cache is cleared is the unrelated stuck-PTZ reboot path — stuck_detector.py:166-168.
Result: after a camera delivers at least one frame, /latest_image keeps returning that stale-but-valid frame, so predict() keeps running and the heartbeat keeps flowing — regardless of whether the camera is reachable now. The heartbeat effectively means "the camera API is up and once had an image for this pose," not "the camera is reachable."
Notes on the edges:
- A camera that never delivered a frame is reported correctly (empty cache → HTTP 204 → None → predict skipped → no heartbeat).
- Heartbeats are also suppressed while an RTSP/SRT stream is active, since the whole inference pass is skipped — core.py:187-198 — so an active stream masks camera health too.
Suggestions to be discussed
- OPTION A : Timestamp the cache (preferred). Store (image, captured_at) in last_images and have /latest_image return 204 (or a stale flag) when the newest capture is older than N patrol cycles. This reuses the existing frame is None gate, so dead cameras naturally stop heart-beating with no new call path.
- OPTION B Decouple heartbeat from the cached frame. Do a real liveness probe of the camera (or track last-successful-capture time in the camera API and expose it) before sending the heartbeat, instead of inferring liveness from a possibly-stale image.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with pyroengine/core.py:187-205 and pyroengine/engine.py:222-225,268-269 to trace how a returned frame triggers prediction and heartbeat. Then inspect pyro_camera_api/pyro_camera_api/api/routes_cameras.py:254-256 and camera/patrol.py:89-96, including the cache reset in stuck_detector.py:166-168. Done means a failed or stale capture no longer causes /latest_image to feed prediction or send a misleading heartbeat.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, observability
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100