Skip to content

Issue report for Squid vendor — is_busy() flag stuck-True #558

Description

@cccoolll

Affected unit
One of REEF lab's four deployed Squid microscopes (microscope received around December 2023)
Variant: Squid v2 simplified (is_squid_plus = false)
Camera: Daheng MER-1220-32U3M (USB-3 industrial mono)
Microcontroller: Teensyduino USB Serial, USB ID 16c0:0483, bcdDevice 2.80, iSerial 12769040
Host: Raspberry Pi 5, Debian 13 trixie, kernel-bundled cdc_acm driver, /dev/ttyACM0
Software: squid_control (REEF-vendored from squid-control repo, git 66cf391-dirty)

Symptom
microcontroller.is_busy() returns True indefinitely. Every code path that polls it in a while self.microcontroller.is_busy(): time.sleep(...) hangs (we saw this in LaserAutofocusController.wait_till_operation_is_completed, AutofocusWorker.wait_till_operation_is_completed, MultiPointWorker.wait_till_operation_is_completed).

Observable from outside
/health returns controller_ready=True, microcontroller_ok=True (the busy state is NOT surfaced)
Stage motion (/move/to, /move/handoff, /move/return) works normally → the busy poll lives only inside the AF/multi-point loops; the navigation controller path does not gate on it
Every autofocus call (/autofocus/contrast, /autofocus/laser, /autofocus/hybrid) hangs indefinitely on the busy-poll loop until our newly-added 10 s timeout fires

What does NOT recover it
Restarting the squid-server Python process (closes & re-opens /dev/ttyACM0)
Restarting the host's cdc_acm USB binding (we could not test this directly today — needs sudo on the host — but on similar incidents in May 2025 the binding cycle alone did not clear it)
The microcontroller.reset() SDK call (sends CMD_SET.RESET) — untested today because the wedge prevents safely entering the reset path

What DOES recover it
Unplugging the Teensy's USB cable for ~30 s and replugging — i.e. a full Teensy power-cycle from the USB bus. The Teensy's onboard 5 V drops, MCU resets cleanly, is_busy() returns to False on next session

Reproduction conditions (not deterministic — hours of normal operation between events)
Has occurred multiple times across our 4 Squid units (squid-1, squid-2, squid+3) over the past 6 weeks
Strongly correlates with rapid back-to-back AF calls — particularly when laser AF runs first and a contrast AF or /plate/probe follows within ~1 s on the same busy_lock. We documented this exact race in plate.py:280 as "return_stage → get_image sequence wedged the laser AF capture (busy_lock held indefinitely)"
Today's instance (2026-06-09 ~15:50) wedged after 3 sequential AF calls on a freshly-loaded plate (deployed our new hybrid AF, ran contrast → laser → hybrid back-to-back)

Specific question(s) for the vendor
Is there a firmware-level health flag the Teensy exposes for "busy state has been True for > N seconds with no command outstanding"? We'd like to detect the wedge in /health rather than discovering it on the next AF call.
Is there a documented re-sync sequence for the SDK to send when it suspects is_busy() is stuck? Ideally a sequence that does not require the Teensy to actually be responsive — e.g. a UART break + handshake, or a magic-byte sequence the firmware checks even when "busy".
Is the underlying race known? When laser-AF's spot capture overlaps with stage-motion set_target_position calls, can the firmware's busy bookkeeping latch True from one path while the other clears it on the wrong cycle? If yes, what's the recommended thread-safety contract — e.g. must all stage motion be serialized through a single host-side mutex with no overlap with laser-AF?
Recommended firmware version for the 850 nm laser-AF retrofit kit. Our bcdDevice 2.80 is whatever shipped with the units in early 2025; if there's a 2.81+ that addresses the busy-latch race, we'd happily flash.

Defensive code we've shipped today (REEF-side, ready to revert if vendor recommends)
Added timeout_s (default 10 s) to all three SDK wait_till_operation_is_completed methods (LaserAutofocusController, AutofocusWorker, MultiPointWorker) so the wedge stops cascading into a server-wide deadlock. On timeout we raise TimeoutError, which lets the async with sq.busy_lock: block release the lock so the next operation can proceed (and surfaces a clear error message instead of a 30 s gateway timeout).
This is a workaround, not a fix — the underlying is_busy=True latch is still there until a Teensy power-cycle. We'd happily replace it with whatever the vendor recommends.

Image

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions