Skip to content

Captures stall ~37 s on alternate frames — reproduces in the gphoto2 CLI, so it is not our code #14

Description

@cmtunderbird

The number

Measured on scanner-node-0, 12 Aug 2026, ~23:00 EEST, immediately after a battery exchange.

POST /capture, back to back, nothing in between:
  1 -> 200 in  1.85s
  2 -> 200 in 37.32s
  3 -> 200 in 37.38s

Flat at ~37 s, not growing — so this is a fixed timeout being waited out, not something accumulating.

At 1.8 s a frame a 300-page book is ~15 minutes of capture per camera. At 37 s it is five hours. This is a throughput-fatal defect, not a nuisance.

It is also new. This afternoon ten consecutive captures ran at ~1.8 s each; three ran at 1.7 s at 22:37. The change is tonight, and the only physical event between the two is the battery going flat and being swapped.

It is not our code

Same method that exonerated the node during the DMF fault: stop the service and go to the CLI.

Three frames in one gphoto2 process (session stays open — the opposite of our session_per_capture):

Saving file as /tmp/multi_1.arw
ERROR: Could not capture image.
ERROR: Could not capture.
Saving file as /tmp/multi_2.arw
  three frames, one session: 39.05s

Three separate gphoto2 processes (session per capture, like the node):

  separate capture 1: 38.43s
  separate capture 2:  2.26s

So it reproduces with our service stopped, under both session strategies, and one capture in two fails outright with Could not capture image before a retry succeeds. The node's capture_with_retry is the only reason this surfaces as a slow 200 rather than a 503 — which is worth saying plainly: our recovery is currently hiding the fault and reporting success. A run would look healthy at a fifth of the throughput.

What it is not

Ruled out by measurement, not by argument:

  • Not our session strategy. One-session and session-per-capture stall identically.
  • Not the body's mode settings. Checked after the swap: focusmode: Manual, capturemode: Single Shot, expprogram: M. The DMF fault from earlier tonight is genuinely fixed and is not this. (This was my first hypothesis — that _enforce_capture_prerequisites() runs in connect() but not on the reconnect() path a battery swap takes, so a reset body would come back in DMF unnoticed. The gap in the code is real and worth closing regardless, but it is not the cause here, because the body is in Manual.)
  • Not files accumulating on the camera. gphoto2 --list-filesThere is no file in folder '/'.
  • Not USB autosuspend. The udev rule pins power/control=on, and usb_resets stayed at 1 throughout.
  • Not the /status polling in the test harness — removing it changed nothing.

The leading hypothesis

The body's volatile buffer, which is the hazard PR #9's duplicate guard was built against, showing up as a throughput failure instead of a correctness one. libgphoto2's own source predicts the mechanism:

"It may even accumulate so we will get images from history. Camera on-off does not delete RAM. Just USB reconnection helps."

The alternating fast/slow pattern fits: a capture leaves something in body RAM, the next capture collides with it and waits out a timeout that clears it, the one after is clean.

This is testable and untested. If it is right, usb_reset() should restore the fast path, and the alternation should resume from there. That is the next measurement, and it decides everything: if a re-enumeration fixes it we have a workaround we already own; if it does not, the cause is elsewhere.

Worth checking in the same sitting

  • Whether a cold body power cycle (not just a USB reset) clears it — the state was introduced by a power event, so it may be clearable by one.
  • Whether the ~37 s matches a specific libgphoto2 timeout constant, which would name the wait exactly rather than by its duration.
  • Whether it survives a reboot of the Pi. If it does, the state lives in the body.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions