Skip to content

fix(ibidi): release multiplexer serial port deterministically on shutdown - #20

Merged
Heerpa merged 3 commits into
developfrom
fix/ibidi-port-release
Oct 21, 2026
Merged

Heerpa merged 3 commits into
developfrom
fix/ibidi-port-release

Conversation

@Heerpa

@Heerpa Heerpa commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

On the rig, after restarting PycroFlow the ibidi MultiFlOW multiplexer could not be reconnected without physically unplugging/replugging the USB and/or power cables.

Root cause

The multiplexer's serial port was only ever closed through garbage-collection-dependent __del__ hooks, which are not guaranteed to run at interpreter exit:

  • The GUI's SystemService.close() (called from closeEvent) did stop_all_moves() + close_imaging() — it never disconnected the fluid system.
  • Even disconnect_fluid() only closed the Hamilton bus (ha.disconnect()); the ibidi is a separate serial link that was never explicitly closed, and then the fluid_system reference was dropped → close fell to GC.

Two failure modes follow, and the fix addresses both:

  1. OS holds the port / lingering handle → graceful, deterministic close releases it.
  2. Device firmware wedged by an abrupt mid-command USB-CDC teardown → flushing before close makes the close graceful.

Changes

  • SystemService.close() now calls disconnect_fluid().
  • disconnect_fluid() calls LegacyArchitecture.close() before tearing down the Hamilton bus and dropping the reference.
  • New LegacyArchitecture.close() — deterministic, idempotent counterpart to __del__; stops the spill sensor and closes the multiplexer.
  • IbidiMultiplexer.close() flushes + clears serial buffers before closing (graceful, not abrupt).
  • IbidiMultiplexer opens with exclusive=True (POSIX lock; no-op on Windows, where COM ports are already exclusive) so a stale handle fails the next open loudly.

Tests

Hermetic (fake serial / MagicMock), all passing: flush-before-close ordering + idempotency; port release via LegacyArchitecture.close(); SystemService.close() disconnects the fluid architecture. (The unrelated test_placeholder_when_monet_absent failure is pre-existing on develop.)

⚠️ Still to confirm on the rig

Which cable actually fixes the reconnect: USB-only replug → OS/handle (this PR is sufficient); needs power cycle → firmware wedge, in which case a follow-up to leave the device idle (and possibly issue a reset command) on close is warranted.

🤖 Generated with Claude Code

Heerpa and others added 3 commits October 21, 2026 17:50
…down

After a PycroFlow restart the ibidi MultiFlOW could not reconnect without
physically replugging USB/power. The port was only closed via GC-dependent
__del__: SystemService.close() never disconnected the fluid system, and
disconnect_fluid() closed only the Hamilton bus (ha.disconnect()), not the
ibidi's separate serial link. Dropping a live USB-CDC handle (possibly
mid-command) can also leave the firmware wedged until a physical replug.

- SystemService.close() now calls disconnect_fluid()
- disconnect_fluid() calls LegacyArchitecture.close() before tearing down the
  Hamilton bus and dropping the reference
- new LegacyArchitecture.close(): deterministic, idempotent; stops the spill
  sensor and closes the multiplexer
- IbidiMultiplexer.close() flushes + clears serial buffers before closing
  (graceful, not abrupt)
- IbidiMultiplexer opens with exclusive=True (POSIX lock; no-op on Windows)
- hermetic tests for all four paths

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses code-review findings on the shutdown path:
- IbidiMultiplexer.close() no longer calls flush() — pyserial flush blocks
  until output drains (termios.tcdrain / out_waiting busy-wait), which on an
  unresponsive device (the exact restart scenario) would hang the entire
  shutdown, uncatchable by the try/except. Output is now discarded via
  reset_output_buffer instead. close() also runs under self._lock so it can't
  swap the handle out from under an in-flight _command on another thread.
- LegacyArchitecture.close() releases each resource independently: a failure
  in _stop_spill_sensor() must not skip mux.close(), since the _closed guard
  allows only one attempt and would otherwise strand the ibidi port.
- tests updated (flush must NOT be called) + new regression: spill-sensor
  failure still releases the port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Heerpa
Heerpa merged commit 7c55bc9 into develop Oct 21, 2026
2 checks passed
@Heerpa
Heerpa deleted the fix/ibidi-port-release branch October 21, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant