Widgets¶
Stateless function widgets you call from inside @app.ui. See Widgets concept page for the contract and the widget gallery for a visual index of all of them on one page.
signal_viewer ¶
signal_viewer(ctx: Context, stream_name: str, size: tuple[float, float] = (-1, -1), n_pixels: int = 2000, channel_height: float = 0.0, show_diagnostics: bool = False, selectable: bool = False, scale_mode: str = 'auto', y_range: tuple[float, float] = (-1.0, 1.0), show_markers: bool = False, window_seconds: float = 5.0) -> None
Real-time multi-channel signal viewer.
Includes decimation, pause, auto/manual Y scale, visual-only display
filters, channel toggles, stats, stream retargeting, and label markers.
The function argument stream_name is the stable widget ID; when
selectable=True, the user may switch the active stream from the UI.
scale_mode supports "auto" for ImPlot fitting and "manual" for the
user-set y_range.
window_seconds sets the initial display window in seconds — the user
can still drag the slider afterwards. Defaults to 5 s, which is wide
enough to scan visually across most real-time setups. Pass a smaller
value when you want the display to mirror a short analysis window
(classification often runs at 0.2 s, for example). The stream's
buffer_seconds must be at least this large.
Source code in myogestic/widgets/signal.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |

raw_signal_viewer ¶
raw_signal_viewer(ctx: Context, stream_name: str, size: tuple[float, float] = (-1, 300), channel_height: float = 0.0) -> None
Raw signal viewer — every sample, no decimation, zero-alloc render path.
Source code in myogestic/widgets/raw_signal.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
recording_controls ¶
recording_controls(ctx: Context, class_names: list[str] | None = None, *, on_record: Callable[[], None], on_stop: Callable[[], None], on_gesture: Callable[[int], None] | None = None) -> None
Record/Stop + per-class label buttons + state pill.
The widget reads ctx and drives recording via the explicit callbacks —
it does not import App. Pass app.start_recording / app.stop_recording
if you're using the standard App.
Clicking a class button while recording snaps a label event at that moment (the active class is shown in the "Recording into: …" header). Outside of recording it just sets the next class to be used when Record is clicked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
Context
|
myogestic Context. Mutated: |
required |
class_names
|
list[str] | None
|
Per-class label-button names. |
None
|
on_record
|
Callable[[], None]
|
Called when Record is clicked (idle → recording). |
required |
on_stop
|
Callable[[], None]
|
Called when Stop is clicked (recording → idle). |
required |
on_gesture
|
Callable[[int], None] | None
|
Optional |
None
|
Source code in myogestic/widgets/recording.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |

session_manager ¶
Session manager widget: load/select sessions and class filters for training.
add_recorded_session ¶
Register a freshly recorded session as selected.
Source code in myogestic/widgets/_session_manager_state.py
session_manager ¶
session_manager(base_path: str = 'sessions', label: str = 'Sessions', class_names: list[str] | None = None) -> TrainingData
Session picker widget. Returns TrainingData(paths, class_names, classes).
The widget has two training filters: selected session files and selected
class indices. Session scanning/state lives in
_session_manager_state.py. Assign the returned value to
pipeline.training_data to make it visible to @pipeline.train::
@app.ui
def ui(ctx):
pipeline.training_data = session_manager(...)
Source code in myogestic/widgets/session_manager.py

process_launcher ¶
Process launcher widget for @app.ui.
Usage
from myogestic.proc import process_launcher
PROCESSES = [ ("8ch EMG", ["mne_lsl_player", "--n_channels", "8", "--fs", "256"]), ("Webcam", [sys.executable, "-m", "myogestic.bridges.webcam", ...]), ]
@app.ui def my_ui(ctx): process_launcher(PROCESSES)
process_launcher ¶
Dropdown + Launch/Stop + scrollable log panel.
Multiple process_launcher() calls can coexist in the same UI — each gets unique ImGui IDs via the label parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
processes
|
list[Process]
|
List of (name, command) tuples. |
required |
label
|
str
|
Unique ID for this launcher instance. Auto-generated if empty. |
''
|
log_height
|
float
|
Height of the inline log panel in pixels. Pass |
-1.0
|
Source code in myogestic/widgets/process_launcher.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |

scatter2d ¶
scatter2d(label: str, points: ndarray, labels: ndarray | None = None, class_names: list[str] | None = None, size: tuple[float, float] = (-1, 300), marker_size: float = 3.0) -> None
2D scatter plot with per-class coloring.
Source code in myogestic/widgets/scatter.py
scatter3d ¶
scatter3d(label: str, points: ndarray, labels: ndarray | None = None, class_names: list[str] | None = None, size: tuple[float, float] = (-1, 400), axis_names: tuple[str, str, str] = ('X', 'Y', 'Z')) -> None
3D scatter plot with orbit camera.
Source code in myogestic/widgets/scatter.py
heatmap ¶
Heatmap widget for @app.ui (confusion matrix, correlation matrix, etc.).
from myogestic.widgets.heatmap import heatmap
heatmap ¶
heatmap(label: str, data: ndarray, size: tuple[float, float] = (-1, 300), label_fmt: str = '%.1f') -> None
2D heatmap.
Source code in myogestic/widgets/heatmap.py
line_plot ¶
Multi-channel line plot for @app.ui.
from myogestic.widgets.line_plot import line_plot
line_plot ¶
line_plot(label: str, data: ndarray, channel_names: list[str] | None = None, size: tuple[float, float] = (-1, 200)) -> None
Multi-channel line plot.
Source code in myogestic/widgets/line_plot.py
FilterControl ¶
Stateful holder for a runtime-tunable :class:VectorFilter.
Renders a self-contained panel with a header, button-style filter selector, parameter controls, and a reset button. Parameters update in place where possible (no rebuild) to preserve smoothing history during live tuning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hz
|
float
|
Sample rate forwarded to |
50.0
|
default
|
str
|
Initial filter name — |
'one_euro'
|
Source code in myogestic/widgets/filter_controls.py
reset ¶
ui ¶
ui(label: str = 'output_filter') -> None
Render the full panel. Call once per frame inside @app.ui.
Source code in myogestic/widgets/filter_controls.py

FeatureSelector ¶
Tickable list of named feature functions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
dict[str, FeatureFn]
|
Ordered map of feature name → callable. Each callable
takes an EMG window |
required |
default
|
Iterable[str] | None
|
Optional iterable of feature names to start ticked.
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if a name in |
Source code in myogestic/widgets/feature_selector.py
active_names
property
¶
Feature names currently ticked, in registration order.
is_active ¶
set_active ¶
Programmatically tick / untick a feature.
Useful for restoring saved selections from a checkpoint, or for scripted training runs that bypass the UI.
Source code in myogestic/widgets/feature_selector.py
ui ¶
Render the panel inside an ImGui frame.
Call from inside @app.ui. Renders a header, the feature
checkboxes laid out in a wrapping grid that reflows with the
panel's current width (one column when narrow, many when wide),
and a footer line showing the active count. State updates take
effect on the next predict-thread tick.
Source code in myogestic/widgets/feature_selector.py

template_inspector ¶
Generic template-row review table.
A reusable accept/reject + click-to-select widget. Caller owns extraction
(the Extract Templates button + worker thread + how to build rows from
sessions); this widget renders the resulting rows and lets the user check
checkboxes and click rows to select one for inspection.
Reusable from any example that wants an "extract → review → train" workflow (regression target picking, NN onset detection, …).
Design notes:
- TemplateInspectorRow is a small mutable dataclass; the widget
toggles accepted in place when the user ticks a checkbox. Caller
reads [r for r in rows if r.accepted] to pull the selected set.
- The widget returns the currently-selected row's key (or None)
so the caller can render a preview / details panel for that row.
- No alias maps, label classification, or model semantics live here —
those stay in user code.
TemplateInspectorRow
dataclass
¶
TemplateInspectorRow(key: str, label: str, accepted: bool = True, info_text: str | None = None, energy: float | None = None)
One row in the inspector table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
Stable identity (e.g. |
required |
label
|
str
|
Short class/category badge (e.g. |
required |
accepted
|
bool
|
Mutable. True = include in training. Toggled in place by the checkbox. |
True
|
info_text
|
str | None
|
Optional secondary text shown in the table (e.g.
session name, source path). May be |
None
|
energy
|
float | None
|
Optional scalar shown as a normalised progress bar in
the energy column. Caller's choice of metric — RMS energy,
peak amplitude, anything monotonic. |
None
|
template_inspector ¶
template_inspector(uid: str, rows: list[TemplateInspectorRow], *, title: str = 'Templates', height: float = 240.0, label_colors: dict[str, tuple[float, float, float, float]] | None = None) -> str | None
Render the table. Returns the selected row's key (or None).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uid
|
str
|
Stable identity string. Two calls with the same uid share selection state across frames; different uids are independent. |
required |
rows
|
list[TemplateInspectorRow]
|
List of |
required |
title
|
str
|
Header text shown above the table. |
'Templates'
|
height
|
float
|
Table height in pixels. |
240.0
|
label_colors
|
dict[str, tuple[float, float, float, float]] | None
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
str | None
|
The |
str | None
|
is selected, or the previously-selected row was removed). |
Source code in myogestic/widgets/template_inspector.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
TemplateInspectorRow
dataclass
¶
TemplateInspectorRow(key: str, label: str, accepted: bool = True, info_text: str | None = None, energy: float | None = None)
One row in the inspector table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
Stable identity (e.g. |
required |
label
|
str
|
Short class/category badge (e.g. |
required |
accepted
|
bool
|
Mutable. True = include in training. Toggled in place by the checkbox. |
True
|
info_text
|
str | None
|
Optional secondary text shown in the table (e.g.
session name, source path). May be |
None
|
energy
|
float | None
|
Optional scalar shown as a normalised progress bar in
the energy column. Caller's choice of metric — RMS energy,
peak amplitude, anything monotonic. |
None
|
trial_preview ¶
Stacked-channel waveform with optional shaded band overlay.
Generic trial / template / segment preview widget. Renders multi-channel biosignal data as stacked traces (per-channel offsets), optionally with a colored band marking a region of interest (e.g. an extracted template, a labeled gesture, a chosen training window).
Reusable from any example that wants a recorded-trial review surface.
Design notes:
- Channels-first (default) or samples-first via data_layout. Live
Stream.get_window() is channels-first; Recording.data from
Session.get_trials() is samples-first — pass "samples_first"
there instead of transposing at the call site.
- Auto-gain (lane = data_range * 1.2) like signal_viewer's Auto
mode, so channels with different amplitudes stay visible. Manual mode
pins lane to y_range.
- Display filters are explicit kwargs (rectify / dc_removal /
rms_env / none), not coupled to a live viewer's state — the
caller decides what to mirror.
trial_preview ¶
trial_preview(uid: str, data: ndarray, fs: float, *, data_layout: Literal['channels_first', 'samples_first'] = 'channels_first', title: str | None = None, size: tuple[float, float] = (-1.0, 240.0), channel_names: list[str] | None = None, band: tuple[float, float] | None = None, band_color: tuple[float, float, float, float] | None = None, gain: float = 1.0, display_filter: Literal['none', 'rectify', 'dc_removal', 'rms_env'] = 'none', scale_mode: Literal['auto', 'manual'] = 'auto', y_range: tuple[float, float] = (-1.0, 1.0), window: bool = False) -> None
Render stacked multi-channel waveform with optional band overlay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uid
|
str
|
Stable identity string for ImPlot (combined into plot ids so
two |
required |
data
|
ndarray
|
Multi-channel signal. Shape |
required |
fs
|
float
|
Sampling rate in Hz, used for the x-axis labels in seconds. |
required |
title
|
str | None
|
Optional header line shown above the plot. |
None
|
size
|
tuple[float, float]
|
ImPlot size as |
(-1.0, 240.0)
|
channel_names
|
list[str] | None
|
Optional per-channel labels. When omitted, channels
are shown as |
None
|
band
|
tuple[float, float] | None
|
Optional |
None
|
band_color
|
tuple[float, float, float, float] | None
|
RGBA in |
None
|
gain
|
float
|
Multiplier applied to each channel before plotting. Match this to your live viewer's gain knob if you want the preview to look like what was on screen. |
1.0
|
display_filter
|
Literal['none', 'rectify', 'dc_removal', 'rms_env']
|
Visual-only transform applied to a copy of
|
'none'
|
scale_mode
|
Literal['auto', 'manual']
|
|
'auto'
|
y_range
|
tuple[float, float]
|
|
(-1.0, 1.0)
|
window
|
bool
|
When |
False
|
Source code in myogestic/widgets/trial_preview.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
panel_header ¶
Render a uniform panel-header line: muted, all-caps, optional FA icon.
Pairs with the button-button + slider styling used by the other widgets in this package. Use it at the top of any custom panel to match the look::
panel_header("MODEL", icons_fontawesome_6.ICON_FA_BRAIN)
train_button(pipeline)
...
The text color follows the active theme's text_disabled slot, so it
reads correctly on both light and dark themes without hardcoding.
Source code in myogestic/widgets/_common.py
popout_panel ¶
popout_panel(title: str, gui_fn: Callable[[], None], *, default_open: bool = True, can_be_closed: bool = True, remember_is_visible: bool | None = None) -> None
Render gui_fn inside a dockable, tearable ImGui window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
title
|
str
|
Window title — also used as the ImGui id and as the dedup key for repeated calls. |
required |
gui_fn
|
Callable[[], None]
|
Zero-arg callable invoked by ImGui every frame. Treat it
like the body of a |
required |
default_open
|
bool
|
Initial visibility of the window on first launch.
Subsequent launches restore from |
True
|
can_be_closed
|
bool
|
Whether the user can close the window with the X button. Closed windows reappear via the "View" menu. |
True
|
remember_is_visible
|
bool | None
|
Whether visibility is persisted in the imgui ini file. Defaults to True for existing behavior. |
None
|
When App(docking=True) is not active, this just runs gui_fn()
inline so the call site stays the same.
Source code in myogestic/widgets/popout.py
Status and logs¶
stream_panel ¶
Per-stream status panel for @app.ui.
A compact replacement for the MyoGestic "device setup" tab that only shows
what's actually true at runtime: source class, connection status, sample
rate, channel count, last-sample age, plus inline connect buttons for any
target the source's discover() reports. Rendering is one-shot per
frame — no hidden state beyond the discovery cache (shared with the signal
viewers).
stream_panel ¶
Render one row per stream with status + metadata + reconnect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
Context
|
App context. |
required |
selectable
|
bool
|
When True and the stream's source supports |
True
|
show_header
|
bool
|
Render a uniform |
True
|
Source code in myogestic/widgets/stream_panel.py
log_panel ¶
General app-event log panel for @app.ui.
Displays whatever lines have been pushed via ctx.log(...). Independent
from pipeline.train_log (model-training only) — this is for high-level
app events (recording saved, model loaded, stream reconnected, process
crashed). The widget is read-only and auto-scrolls to the latest line when
the user is already pinned to the bottom.
log_panel ¶
log_panel(ctx: Context, height: float = -1.0, title: str = 'App Log', show_header: bool = True) -> None
Render the app log as a scrollable, read-only panel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
Context
|
App context; reads from |
required |
height
|
float
|
Panel height in pixels. Pass a value |
-1.0
|
title
|
str
|
Header label (only shown when |
'App Log'
|
show_header
|
bool
|
Render the button-style |
True
|
Source code in myogestic/widgets/log_panel.py
Branding¶
app_logo ¶
Render the MyoGestic wordmark, fit-to-cell, aspect-preserving.
The widget reads the available content area inside the current panel and renders the wordmark as the largest aspect-preserving rectangle that fits both dimensions (minus the requested padding), then centres it. So in a cell whose aspect matches the wordmark, the image fills edge-to-edge minus the padding margin; in a cell that's a different aspect, the image fills the tighter dimension and leaves extra balanced padding along the other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
float | None
|
Optional cap on the wordmark's width in pixels. |
None
|
padding
|
float
|
Margin in pixels reserved on every side. Default 12 px gives the wordmark breathing room against the panel border. |
12.0
|
Uses image_and_size_from_asset + raw imgui.image rather than the
higher-level image_from_asset(..., size=...) helper, which in this
version of hello_imgui ignored the explicit size and rendered at the
natural pixel dimensions of the PNG.
Source code in myogestic/widgets/app_logo.py

ML readout¶
prediction_label ¶
prediction_label(pipeline: Pipeline, class_names: Sequence[str], *, key: str = 'class', proba_key: str = 'proba', label: str = 'Prediction', show_probability: bool = False, font_scale: float = 2.0) -> None
Render the current predicted class name as a big centred label.
The class index is looked up in pipeline.predictions[key] and the
name is taken from class_names. Colour-codes each class with the
shared :data:myogestic.widgets._common.PALETTE so the same class is
always the same colour (matches the recording / session-manager
chips).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pipeline
|
Pipeline
|
The Pipeline whose predictions to read. Untrained or
first-frame state ( |
required |
class_names
|
Sequence[str]
|
Class names indexed the same way as the model —
|
required |
key
|
str
|
Dict key in |
'class'
|
proba_key
|
str
|
Dict key holding the per-class probability vector,
consumed only when |
'proba'
|
label
|
str
|
Panel header text. |
'Prediction'
|
show_probability
|
bool
|
When True, render a coloured progress bar of the predicted class's probability below the name. |
False
|
font_scale
|
float
|
Multiplier applied to the class name's text size. Defaults to 2× the panel font. |
2.0
|
Source code in myogestic/widgets/prediction_label.py

Virtual Hand integration¶
VhiMovementPanel ¶
VhiMovementPanel(client: VhiControlClient, *, on_movement: Callable[[str], None] | None = None, refresh_min_interval_s: float = 1.0, label: str = 'VHI Movements')
Stateful widget — instantiate once at module level, call .ui() per frame.
Example::
panel = VhiMovementPanel(vhi_client)
@app.ui
def ui(ctx):
with grid[8, 0]:
panel.ui()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
VhiControlClient
|
The :class: |
required |
on_movement
|
Callable[[str], None] | None
|
Click handler for a movement button. Defaults to
|
None
|
refresh_min_interval_s
|
float
|
Minimum seconds between background state refreshes. Default 1 s. |
1.0
|
label
|
str
|
Panel header text rendered above the button grid. |
'VHI Movements'
|
Source code in myogestic/widgets/vhi_movement_panel.py
ui ¶
Render the panel — call once per frame inside @app.ui.
Source code in myogestic/widgets/vhi_movement_panel.py

Lower-level pieces¶
VhiMovementPanel wraps these for the common case. Reach for them directly when you want to share one state cache across multiple panels, or render the palette without owning a client.
vhi_movement_palette ¶
vhi_movement_palette(movements: Sequence[str], *, on_movement: Callable[[str], None], on_refresh: Callable[[], None] | None = None, current_movement: str = '', connected: bool = False, status: str = '', label: str = 'VHI Movements') -> None
Render VHI's movement names as a grid of command buttons.
Pure ImGui: performs no RPC and owns no client. movements is the cached
list from the last successful GetState; on_movement(name) fires on
click (wire it to VhiControlClient.set_movement). If on_refresh is
given, a refresh button is drawn. Movement buttons are disabled while
connected is False, but a stale list stays visible.
The grid uses as many columns as fit the panel width, so it reflows when
the panel is resized; the button matching current_movement is highlighted.
Source code in myogestic/widgets/vhi_movement_palette.py
VhiStateCache
dataclass
¶
VhiStateCache(movements: list[str] = list(), current_movement: str = '', current_state: str = '', mode: str = '', connected: bool = False, refreshing: bool = False, message: str = 'Launch VHI, then refresh.', last_attempt_s: float = 0.0, lock: Lock = Lock())
Last-known VHI state, refreshed off-thread. Use snapshot() to read.
snapshot ¶
snapshot() -> VhiStateSnapshot
Return a consistent, immutable view — safe to read all frame.
Source code in myogestic/widgets/vhi_movement_palette.py
VhiStateSnapshot
dataclass
¶
VhiStateSnapshot(movements: tuple[str, ...], current_movement: str, current_state: str, mode: str, connected: bool, message: str)
An immutable, lock-free view of VhiStateCache for one UI frame.
request_vhi_state_refresh ¶
request_vhi_state_refresh(client: VhiControlClient, cache: VhiStateCache, *, force: bool = False, min_interval_s: float = 1.0) -> None
Start at most one throttled background GetState refresh.
Safe to call every frame from @app.ui: it returns immediately unless a
refresh is due (min_interval_s elapsed, or force) and none is
already in flight. The blocking get_state() runs on a daemon thread;
the result lands in cache under its lock.