feat(bpod): serial-safe live-command injection into a running trial - #77
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_machinepoll loop. Issuing amanual_overridefrom 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).submit()is called concurrently from multiple threads.Safety envelope
CommandPolicyis 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 untildrainruns 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.pycovers 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.