pycontrol-gui · architecture
Core owns board I/O and workspace parsing. The GUI adds Qt widgets, worker threads,
plotting, and tab workflows — connected through one QtBusAdapter.
flowchart TD
subgraph gui ["pycontrol-gui · top"]
MW["MainWindow"]
WM["WorkspaceModel"]
RTab["RunTaskTab"]
STab["SetupsTab"]
ETab["ExperimentsTab"]
RunView["ExperimentRunView"]
Panel["SubjectRunPanel(s)"]
SharedRT["shared SessionRuntime"]
SubjectRT["per-subject SessionRuntime(s)"]
RTI["SessionRuntime internals (per instance)"]
Cmds["SessionCommands · GUI thread"]
QBA["QtBusAdapter · GUI-thread QObject"]
Ctrl["SessionController · worker QThread"]
MW --> WM
MW --> RTab
MW --> STab
MW --> ETab
MW --> SharedRT
ETab --> RunView --> Panel --> SubjectRT
SharedRT -.-> RTI
SubjectRT -.-> RTI
RTI --> Cmds
RTI --> QBA
RTI --> Ctrl
RTab -->|"commands"| Cmds
STab -->|"setup actions"| Cmds
Panel -->|"commands"| Cmds
Cmds -->|"queued command signals"| Ctrl
Ctrl -->|"status / setup signals"| RTab
Ctrl -->|"setup signals"| STab
Ctrl -->|"status signals"| Panel
QBA -->|"queued bus signals"| RTab
QBA -->|"queued bus signals"| Panel
end
subgraph core ["pycontrol-core · bottom"]
WS["Workspace"]
BS["BoardSession"]
T["SerialTransport"]
Reg["SubscriberRegistry"]
Pump["pump thread · _pump_loop"]
Dec["FrameDecoder"]
Log["NativeEventLogger"]
Ext["TaskExtension"]
BS --- T
BS --> Reg
BS -.->|"start()"| Pump
Pump -->|"read bytes"| T
Pump --> Dec
Dec -->|"dispatch"| Reg
Reg --> Log
Reg --> Ext
end
WM -->|"discover / reload"| WS
Ctrl -->|"creates + drives"| BS
Ctrl -->|"add_subscriber(QBA) at connect"| BS
Ctrl -->|"add log / extension subscribers at start"| BS
Reg -->|"subscriber calls on pump thread"| QBA
MainWindow owns one
shared SessionRuntime for Run task and Setups; each
SubjectRunPanel owns another runtime with the same internals.
Inside each runtime, only SessionController is moved to a
worker QThread — SessionCommands and
QtBusAdapter stay on the GUI thread. The controller creates
and drives core BoardSession, registers QtBusAdapter
at connect, and adds loggers / TaskExtension at
start(). During the run, BoardSession's pump
thread reads serial, dispatches bus events, and invokes
QtBusAdapter subscriber methods from that pump thread.
GUI wrapper around core Workspace discovery. Emits Qt signals for tasks, hwdefs, setups, ports, and experiments. Reloads once per second on a short-lived worker, applies snapshot on the GUI thread.
Bundles SessionCommands, QtBusAdapter, and SessionController + QThread. Shared runtime wired from MainWindow; experiment panels each construct their own.
GUI-thread QObject implementing the core subscriber protocol. Registry invokes it from the pump thread; it emits Qt signals that tabs receive via explicit QueuedConnection.
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.