Skip to content

feat(bpod): serial-safe live-command injection into a running trial - #77

Merged
larsrollik merged 1 commit into
mainfrom
ft/bpod-live-injection
Aug 17, 2026
Merged

feat(bpod): serial-safe live-command injection into a running trial#77
larsrollik merged 1 commit into
mainfrom
ft/bpod-live-injection

Conversation

@larsrollik

Copy link
Copy Markdown
Member

Adds a serial-safe way to issue a live command (an extra reward / valve pulse) into a running Bpod trial, without corrupting the serial stream.

Why

The Bpod serial connection is owned by the task thread inside pybpodapi's run_state_machine poll loop. Issuing a manual_override from any other thread races that loop's own I/O and wedges the connection. The only serial-safe injection point is inside the loop, on the same thread — which is exactly what MATLAB does (single-threaded cooperative injection).

What

  • BpodFactory.add_loop_handler(fn) — register callbacks that run once per iteration of the run loop, routed through a dispatch registry that isolates handler errors so one bad handler can't break the loop. This is the serial-safe seam.
  • LiveInjector + CommandPolicy (hardware/bpod/injection.py):
    • submit(cmd) — callable from any thread. Validates the command against a rig-local safety envelope (CommandPolicy: action + valve allowlist, max reward duration, rate limit) and enqueues it. Never touches hardware.
    • drain() — registered as a loop handler, runs on the task thread. Applies commands non-blocking: open the valve now, close it on a later iteration when the pulse has elapsed, so the poll loop never stalls for a pulse.
    • flush_pending() — force-closes any still-open timed valve (between trials / on teardown).
    • Applied overrides are emitted to an audit callback: an override does not appear in the trial's Events/States, so recording it is a data-completeness requirement (reward accounting), not only security.
  • The rate-limit check-and-set is lock-guarded so the envelope still holds when submit() is called concurrently from multiple threads.

Safety envelope

CommandPolicy is enforced locally by the rig — a compromised or buggy caller cannot deliver an unbounded reward or drive an unlisted valve. Defaults deny everything except an explicitly allowlisted reward valve within a bounded duration and rate.

Tests

tests/test_bpod_injection.py (SimBpod, no hardware) covers the policy envelope, the submit/drain split (nothing applied until drain runs on the task thread), the non-blocking timed valve action, concurrent-submitter rate-limit safety, audit emit, and the loop-handler registry. tests/test_bpod_override.py covers the underlying override API + action driver.

The mechanism has also been exercised end-to-end on real hardware (Bpod fw22) during a live sequence session: extra rewards submitted from an external thread were applied mid-trial by the run loop with no serial corruption, and out-of-envelope commands were rejected.

Adds a channel to issue an extra reward or valve pulse mid-trial without
corrupting the Bpod serial stream.

- BpodFactory.add_loop_handler: register callbacks that run once per
  iteration of run_state_machine's poll loop, routed through a dispatch
  registry that isolates handler errors. The loop runs on the task thread
  that owns the serial connection, so a handler is the only serial-safe
  place to apply a manual_override without racing the state machine's I/O.
- LiveInjector + CommandPolicy (hardware/bpod/injection.py): submit()
  validates a command against a rig-local safety envelope (action/valve
  allowlist, max reward duration, rate limit) and enqueues it from any
  thread, never touching hardware; drain() runs on the task thread via a
  loop handler and applies commands non-blocking (open now, close on a
  later iteration) so the poll loop never stalls for a pulse. Applied
  overrides are emitted for an audit log, since they do not appear in the
  trial data.
- The rate-limit check-and-set is lock-guarded so the envelope holds when
  submit() is called concurrently.
- Tests cover the policy envelope, the submit/drain split, the
  non-blocking timed valve action, audit emit, and the loop-handler
  registry.

Minor bump (feat).
@larsrollik
larsrollik requested a review from a team as a code owner August 14, 2026 12:10
@larsrollik
larsrollik merged commit 6519021 into main Aug 17, 2026
7 checks passed
@larsrollik
larsrollik deleted the ft/bpod-live-injection branch August 17, 2026 08:25
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