refactor(display): run() stage 2 - an Arbiter decides the scheduled-off blank, follower and WiFi notice (no behaviour change) - #733
Conversation
…ff blank, follower and WiFi notice, no behaviour change Adds src/display_arbiter.py: Source, ArbiterInputs, ArbiterState, WifiNotice, ScreenPlan and a pure Arbiter.decide(state, inputs, now) (no I/O, no clock reads, nothing mutated). It applies the scheduled-off gate (blank unless an on-demand session overrides it, as #714 left it), then Follower, OnDemand and Wifi in that order; on-demand, live, Vegas and the rotation return a LEGACY plan that runs run()'s existing code. The WiFi notice's mid-screen rule moves there as wifi_notice_preempts. run() gathers the snapshot after the schedule and brightness bookkeeping (_arbiter_inputs; the notice is read only when it could win, since reading it has side effects), calls decide() and dispatches on plan.source. The dwells (60 s blank, 0.5 s notice) come from the plan. Side effects keep their order within a pass. The golden traces regenerate byte-identical. New test/test_display_arbiter.py: a written-out table of all 16 input combinations, the mid-screen table, purity checks and the controller's snapshot through an on-demand override. Three handover tests and a source-order test follow the helpers' new signatures. 23 mutants, all killed. display_arbiter is on the mypy ratchet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (8)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe display loop now builds an input snapshot and uses a stateless arbiter to select scheduled-off, follower, WiFi-notice, or legacy handling. The controller applies plan dwell durations, and WiFi notice preemption uses a shared expiry-aware rule. Tests cover the decision table and controller snapshot behavior. ChangesDisplay Loop Arbitration
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant DisplayController
participant SyncManager
participant Arbiter
DisplayController->>SyncManager: Read follower state
SyncManager-->>DisplayController: Return follower state
DisplayController->>Arbiter: decide with the pass input snapshot
Arbiter-->>DisplayController: Return ScreenPlan
Merge Risk: ⚪ Minimal · up to No actionable regression remains identified for this change. It is ready to merge after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The reviewed display priorities and rendering operations are preserved, and no new security exposure was demonstrated. Concurrent handovers and the trust provenance of external inputs remain incompletely established. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 5 files. (3 skipped: 3 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 9 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Stage 2 of the
run()restructure indocs/RUN_LOOP_REDESIGN.md. No behaviour change.Design
New module
src/display_arbiter.py, added to the mypy ratchet:Source:SCHEDULED_OFF,FOLLOWER,WIFI,LEGACYArbiterInputs(frozen):schedule_on,on_demand_active,follower_active,wifi_notice: Optional[WifiNotice]ArbiterState(frozen, empty for now: none of the stage-2 Sources remembers anything between passes)ScreenPlan(frozen):source,max_duration(60 s for the blank and 0.5 s for the notice, the constantsrun()used to hard-code),noticeArbiter.decide(state, inputs, now) -> ScreenPlan. It is pure: no I/O, no clock reads, no mutation. The order is:LEGACY, which is stillrun()'s code, and it outranks the notice.LEGACY.wifi_notice_preempts(notice, on_demand, now)is the Wifi Source's mid-screen rule._wifi_notice_pendingcalls it.nowis passed but not read. Before this change the top-of-pass WiFi check never compared the notice's expiry, and a table row stopsdecide()from starting to.What changed in
run()run()callsArbiter.decide(ArbiterState(), self._arbiter_inputs(), time.time())and dispatches onplan.source:SCHEDULED_OFF→_blank_while_scheduled_off(plan.max_duration)FOLLOWER→_run_follower_frame()WIFI→_show_wifi_notice(plan.notice, plan.max_duration)LEGACY, or a notice that fails to draw → the existing live / Vegas / rotation code, unchanged_arbiter_inputs():schedule_on = is_display_active and not on_demand_schedule_override. With this, the gate blanks exactly whenis_display_activeis False, as before. A test walks this through an on-demand override, the session ending in the same minute, and the schedule turning on during a session._check_wifi_status_messagehas side effects (its 1 Hz throttle, deleting an expired file) that those passes never had._publish_current_mode_state_if_changed,_apply_pending_vegas_initandprocess_deferred_updatesrun at the same points relative to the branches, so side effects within a pass keep their order.run(), the WiFi, blank and follower helpers, and the new module. Nothing touches state publishing or the command drain, which the control socket stage 3 branch edits.Tests
test/test_display_arbiter.py(56 tests):is_display_activethrough an on-demand sessiontest_handover_scroll_state.py::TestTheControllersOwnScreenstest_display_controller_shutdown_and_state.py, which now checks that_apply_pending_vegas_init()comes before_run_follower_frame()instead of beforeis_follower_active(); that read moved into_arbiter_inputsrun(). Every mutant failed at least one test.Verification
LEDMATRIX_REGEN_GOLDEN=1regenerates all 15 fixtures byte-identical to the committed blobs (compared withgit cat-file).test_run_loop_golden.py,test_run_loop_wifi_and_live.pyandtest_wifi_notice_preemption.pypass unchanged.08469734) baseline run in a separate worktree. Both runs have the same 61 failed and 6 errors (the pre-existing Windows set), with identical FAILED/ERROR IDs. The branch has +56 passed, which are the new tests.plugin-repos/is clean.scripts/check_types.pywith mypy 1.20.2 reports no issues in 94 modules, includingsrc/display_arbiter.py.Still to do (not run: these need the rigs)
python3 scripts/frame_soak.py --previewfor 10 minutes on a scrolling rotation, against main, alternating which build goes first. Compare the late-frame rate and freezes. Note that plain main already fails the default gate on ledpi (~0.11–0.16% late), so compare against main's arms rather than the absolute gate.Follow-up needed right after merge
src/display_arbiter.pyis a new coresrcmodule. The ledmatrix-pluginsscripts/check_min_core_version.pyMODULE_FIRST_VERSIONtable needs a follow-up PR adding"src.display_arbiter": None; until then, every plugins PR's safety job goes red. It has to come after this merges, because aNoneentry must already exist on core main.Stage 3 (next)
ScreenRunnerwithFrameClock,ExitReasonandPREEMPTED, plus the OnDemand, Live and Rotation Sources, so thatLEGACYmeans only Vegas:ArbiterStategains the rotation index, the on-demand list, index, expiry and pin, and the live resume point.ArbiterInputsgains the live modes and the Vegas flags.ScreenPlangainsmode,plugin, the min and max durations,dynamic,frame_policyandpreemptible_by._screen_preempted,_check_live_takeover,_wifi_notice_pending) become a singledecide()call at the runner's service points.Stage 3 touches frame pacing, so it needs the ledpi soak A/B. The details are in
docs/RUN_LOOP_REDESIGN.md.🤖 Generated with Claude Code
Summary by CodeRabbit