pycontrol-gui · architecture
Core owns board I/O and workspace parsing. The GUI adds Qt widgets, worker threads,
plotting, and tab workflows. Commands reach core through
SessionController; bus events return through a per-runtime
QtBusAdapter.
Read these as nested diagrams. The first diagram shows where runtime boxes appear in the GUI. The second opens one runtime box and shows how commands and core bus events cross the Qt boundary.
flowchart TD
MW["MainWindow"] --> WM["WorkspaceModel"]
subgraph tabs ["tabs"]
direction LR
RunTab["Run task"]
SetupsTab["Setups"]
ETab["Experiments"]
end
MW --> RunTab
MW --> SetupsTab
MW --> ETab
RunTab --> SharedRT["shared SessionRuntime"]
SetupsTab --> SharedRT
ETab --> RunView["ExperimentRunView"]
RunView --> Panels["SubjectRunPanel(s)"]
Panels --> SubjectRT["per-subject SessionRuntime(s)"]
WM --> CoreWorkspace["core workspace/config APIs"]
SharedRT -.-> RuntimeBox["see runtime detail below"]
SubjectRT -.-> RuntimeBox
flowchart TD
Receiver["RunTaskTab or SubjectRunPanel"]
subgraph runtime ["one SessionRuntime"]
direction TB
Cmds["SessionCommands"]
Ctrl["SessionController"]
QBA["QtBusAdapter"]
Cmds -->|"queued commands"| Ctrl
end
CoreSession["core session APIs"]
Receiver -->|"command signals"| Cmds
Ctrl -->|"connect/upload/start/stop"| CoreSession
Ctrl -.->|"adds adapter as core subscriber"| CoreSession
CoreSession -->|"bus callbacks"| QBA
QBA -->|"queued Qt signals"| Receiver
flowchart LR
Controls["task controls / GUI action"]
Commands["SessionCommands"]
Controller["SessionController"]
BoardAPI["BoardSession.set_variable() / trigger_event()"]
Adapter["QtBusAdapter"]
Receiver["RunTaskTab or SubjectRunPanel"]
Controls -->|"set variable / trigger event"| Commands
Commands -->|"queued Qt signal"| Controller
Controller -->|"core API call"| BoardAPI
BoardAPI -.->|"running task updates"| Adapter
Adapter -->|"queued Qt signal"| Receiver
SessionRuntime is lifecycle and wiring: it owns
SessionCommands, SessionController,
QtBusAdapter, and the worker QThread.
SessionController is the object that performs session
operations against core. QtBusAdapter is the event bridge back
from core for active runs. Setups shares the runtime for commands and
maintenance, but active-run bus events go to Run task and subject panels.
Host controls use the command path on the way down; they only involve
QtBusAdapter later if the running task emits bus events in
response.
GUI wrapper around core workspace/config APIs. Discovers tasks, hwdefs, setups, ports, experiments, settings, and data paths, then emits Qt signals for the tabs.
Lifecycle wrapper for one controllable board workflow. Owns SessionCommands, SessionController, QtBusAdapter, and the worker QThread.
GUI-thread signal facade. Tabs and controls emit command signals here so Qt can queue them safely to the controller thread.
Worker-thread object that owns the current core BoardSession. Connects, uploads, starts/stops, sets variables, triggers events, syncs firmware, and emits status/error signals.
Event bridge from core back to Qt. Core calls its subscriber methods from the pump thread; it re-emits typed Qt signals for run data, variable changes, warnings, errors, and session start/end.
While a session is running, each runtime uses three threads.
Before start(), only the first two exist — the pump thread is
created when the run begins and stops when the run ends.
Qt's main thread. Owns widgets, SessionCommands, and QtBusAdapter (only the controller is moved off this thread).
SessionController lives here. Connect, upload, start/stop, variable writes, and run-end cleanup.
A plain threading.Thread inside core BoardSession — not a QThread. Reads serial, decodes frames, calls subscribers.
SessionCommands carries commands down (GUI → controller
QThread). QtBusAdapter carries bus events up (pump thread
→ GUI): its QObject lives on the GUI thread, but the registry
calls its subscriber methods from the pump thread before queued signal
delivery. SessionController also emits status signals
(connected, run started/stopped, errors) to tabs from the controller thread.
| Direction | From thread | Crossing | To thread |
|---|---|---|---|
| Commands down | GUI | SessionCommands signal → controller slot |
Controller QThread |
| Bus events up | Pump | QtBusAdapter emit → tab slot (QueuedConnection) |
GUI |
| Controller status | Controller QThread | SessionController signals → tab slots |
GUI |
The pump loop (BoardSession._pump_loop) is the acquisition path
that upstream pyControl used to drive from the Qt event loop:
read bytes from SerialTransport
→ FrameDecoder.feed() # pure sans-IO codec
→ BoardSession._dispatch() # typed bus events
→ SubscriberRegistry # fan-out to all subscribers
→ QtBusAdapter.on_data() → emit Qt signals (still on pump thread)
→ TsvLogger / NativeEventLogger
→ TaskExtension.on_task_data()
QtBusAdapter.on_data() runs on the pump thread; the queued
connection is what moves the result to GUI slots for logs, controls, and
plots. A buggy subscriber is caught and re-emitted as an Error
— it never crashes the pump. Task extensions also run on the pump thread
(stay fast, no Qt widgets). Experiment extensions use a
ThreadPoolExecutor instead.
| Flow | Path |
|---|---|
| Workspace refresh | WorkspaceModel → all three tabs |
| Run task / setup commands | Tab → shared SessionCommands → SessionController |
| Session events | Shared QtBusAdapter → RunTaskTab (logs, controls, plots) |
| Experiment commands | Panel → per-subject SessionCommands → SessionController |
QSettings, not settings.json.
Select setup/task/subject, connect, upload, run. Events feed logs, PlotterHost, and task controls (JSON or Python plugins). Hwdef selector only when the task imports hardware_definition.
Edit experiment JSON; ExperimentRunView runs one isolated SubjectRunPanel per rig. Optional ExperimentExtension plugins get lifecycle hooks off the GUI thread.
Edit setups.json, auto-probe MCU labels, stamp expected_mcu on connect. Bulk board actions route through the shared runtime.
flowchart LR
BoardSession["BoardSession"] --> QtBusAdapterData["QtBusAdapter.events_batch"]
QtBusAdapterData --> RunTaskSlot["RunTaskTab.on_events_batch"]
RunTaskSlot --> PlotterHostBatch["PlotterHost.process_batch"]
RunTaskSlot --> ControlsData["controls process_data"]
RunTaskSlot --> LogViewData["LogView rows"]
PlotTimer["plot_update_timer"] --> PlotterHostUpdate["PlotterHost.update_plot"]
PlotterHost loads plugins/task_plotters/ from v.task_plotter; all methods on the GUI thread (35 Hz default).settings.json; plugin SETTINGS → settings.extensions.ErrorLog.txt, View → Error log.plotting, error_logging, resources. Rest is internal.