Empty Detections and Offline Detectors
A local Fugleramme outage probe checks the difference between a quiet bird detector and a broken connection, including a Windows startup failure.
I disconnected a fake bird detector after Fugleramme rendered one page. The next two iterations kept the PNG byte for byte. I had set the API cache lifetime to zero, so a cached detection could not hide the outage.
That small test covers a useful part of an ML appliance: the code between a prediction service and the thing a person sees. A classifier can return no detections. A broken connection can return no answer. If the application handles both as an empty list, a network blip can erase the last useful result.
Fugleramme puts recent bird detections on an e-ink frame. BirdNET-Go owns audio classification; Fugleramme reads its HTTP API, chooses illustrations, and renders a page. I inspected version 0.22.1, pinned to commit d6baeef3bc7209e43c18476a17112aa494288808, and ran the application code with Python 3.12.6 on Windows. My detector responses and artwork were synthetic. I did not run a microphone, BirdNET weights, or an e-ink panel.
Two Responses at the HTTP Boundary
The package includes a fake BirdNET-Go server that speaks the same /api/v2 routes as the client expects. That lets a test cross a real loopback HTTP connection without loading a model. My downloadable probe starts one healthy server with no rows and another that answers with status 503.
Both clients ask for the all-time species summary. The result:
healthy_empty: []
http_503: Unavailable GET /analytics/species/summary answered 503
The distinction comes from ApiSource. It rejects a non-200 response before interpreting the body. Its cache holds successful values and Unavailable exceptions as different objects. A repeated failure remains a failure; the client does not replace it with an empty collection.
The normal cache lifetime is three seconds. Caching an exception for that interval avoids repeating connection work for several consumers of the same page. It also delays a fresh attempt, which is a tradeoff you should include in any recovery-time measurement. I disabled that cache in the render-loop probe to test the display policy on its own.
I also invoked the actual diagnostic CLI against the healthy, empty fake. These are selected lines from its output:
check_cli_exit: 0
ok reachable answered
ok species, 6 hours 0 species
ok species, 24 hours 0 species
ok species, all time 0 species
ok latest detection none
ok life list none
ok name languages en, nb, sci
all good
Zero birds is a successful query. The diagnostic asks for data the frame uses, including language availability, instead of treating a responding health endpoint as proof that the whole integration works.
The First Run Stopped Before Rendering
From the pinned checkout, I installed the locked dependencies and ran the focused upstream tests:
uv sync --locked --python C:\Python312\python.exe
.venv\Scripts\python.exe -X utf8 -m pytest -q `
tests/test_api.py tests/test_service.py tests/test_check.py
3 failed, 28 passed in 16.33s
All three failures entered service.run. They stopped at its diagnostic signal registration:
src\fugleramme\service.py:110
AttributeError: module 'faulthandler' has no attribute 'register'
The service calls the Unix stack-dump hook before initializing the panel or entering the loop. On this Windows interpreter, faulthandler.register does not exist; Windows also lacks SIGUSR1. The project's workstation development path therefore has a platform assumption before any display hardware gets involved.
I left the checkout unchanged. In the separate probe, I replaced that diagnostic hook with a no-op and supplied the missing signal constant. I also stubbed the panel, the kiosk server, and release checks. Those substitutions let the real detector client, render function, and service loop run in a bounded test. They do not establish that the unmodified Windows service starts.
One Render, Three Iterations
The probe supplies a blackbird detection and a small generated rectangle as its illustration. The first iteration queries the fake API and writes a 1600 × 1200 frame. At the first sleep boundary, the probe hashes the file and shuts down the detector server. It hashes the file again after each of the next two iterations, then stops the loop.
Run the downloaded script from the prepared checkout:
.venv\Scripts\python.exe -X utf8 path\to\outage-probe.py
{
"render_calls": 1,
"ticks": 3,
"unique_frame_hashes": 1,
"api_cache_ttl": 0,
"panel": "stubbed"
}
I checked file identity, not whether the replacement illustration looked like a bird. The assertion proves that these failed polls left the rendered artifact intact.
In the service loop, the application keeps the previous render key when it catches Unavailable. It logs the transition into an outage and leaves the image alone. A successful empty response can reach the normal rendering path and produce an empty page when the mode's key changes. The exception path cannot make that decision.
| Input or event | Observed behavior | Evidence boundary |
|---|---|---|
| Healthy API with zero rows | Empty result; diagnostic exit 0 | Local fake HTTP server |
HTTP 503 | Unavailable exception | Actual client over loopback |
| Detector stops after rendering | One render across three iterations; one unique file hash | Real loop with diagnostic and hardware substitutions |
| Windows service startup | Three upstream tests fail before the loop | Unmodified source |
A Held Page Belongs to One Detector
Caching the last image creates another question: which detector produced it?
The kiosk handler tracks the source URL alongside its last successful PNG. During an outage at the same source, it can serve those bytes. After a source URL change, it clears the held page. Otherwise a user could point the frame at a different station and keep seeing the previous garden's birds.
I ran the two upstream HTTP tests for those cases:
.venv\Scripts\python.exe -X utf8 -m pytest -q tests/test_server.py `
-k 'holds_its_last_page or pointing_at_another'
2 passed, 21 deselected in 9.04s
The outage test first confirms that another data route returns 503, then checks that the image route still returns the original bytes. The source-switch test expects 503 from the new, unreachable station instead of accepting the old image. This is a stronger check than unplugging a service while its TTL cache still has an answer.
Holding the page has a cost: a viewer can see stale detections. The service code retains the image through an outage without a maximum hold age. For a decorative bird frame, that is a defensible choice. A display that informs an operational decision would need a visible freshness policy as well.
The part I would reuse is the three-way result contract: detections, a successful empty result, or an unavailable source. The application can then choose a display policy for each state and test it without making claims about classifier accuracy.