ET UI service v1 (SDK 0.4 / M2)
<epictuner/ui.h> is the optional, Qt-free C service. Declare ui and
et.ui.v1 in the development manifest, query ET_SERVICE_UI with
ET_SERVICE_VERSION_1, and install one dispatcher listener. ABI 1.1 and its
1.0 prefixes are unchanged. <epictuner/sdk/ui.hpp> adds owning PanelBuilder
storage and the Ui convenience client. The shipped hello-panels example
is compilable author code, with no private host includes or QML.
Build, install for development, and open
cmake -S examples/hello-panels -B hello-out -G Ninja -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build hello-out
epictuner --plugin-development --plugin-manifest /absolute/hello-out/plugin.json
Keep the generated plugin.json beside hello-panels.dll (Windows x64) or
hello-panels.so (Linux x64/ARM64). For a multi-config generator, copy the
manifest beside the library in Release. Installing this M2 development example
means copying this directory and loading its manifest. Persistent unsigned
enablement, signed package installation/update and licenses are M5.
Open Plugins → Examples/Hello panels. The example provides editable fields,
a two-way name binding, conditional controls, selectable tabs, results and a chart,
a control gallery, two independent inspectors, and a parent-modal dialog.
Closing/reopening restores opted-in values, selected tabs, splitter ratio, focus,
placement and geometry. Restart only restores compatible visible non-modal panels;
it never repeats a previous button callback. Fault commands exist only when built
with the repository test fixture's ET_HELLO_TEST_FAULTS definition.
Calls, results, and lifetime
All service calls require the worker dispatcher. Background jobs post their
results to that dispatcher before updating UI. Define calls copy the panel,
nodes, property spans and strings before returning; no author pointer crosses
IPC. A definition is immutable and unique for the worker session. The panel ID
must start with the plugin ID plus .; node IDs are unique within a definition.
Parents may appear before or after children, but cycles and depth >32 are rejected.
ET_OK from a call means enqueued, not host acceptance. Pass
et_async_options to receive acceptance/rejection through et_completion.
The C++ Ui wrapper catches listener exceptions inside the plugin compiler and
returns ET_ERROR_CALLBACK; the worker then fails visibly. C listeners return an
et_result. Completion callbacks must be noexcept and must never throw across the
C ABI (especially across compilers); the example logs rejections explicitly.
Completion context and callback code must remain alive until completion or stop.
Stop cancels pending completions before the plugin's stop callback. An operation
that misses its deadline terminates the worker; it is never retried automatically.
| Operation | Host result |
|---|---|
define_panel |
validates the complete definition before registering menu/navigation |
open |
handle in completion.value (ET_VALUE_HANDLE); OPENED event follows |
patch |
atomic property batch; accepted/current revision in completion.value (ET_VALUE_U64) |
action(FOCUS/CLOSE) |
focuses the existing instance or begins asynchronous close |
action(ALLOW_CLOSE/DENY_CLOSE) |
answers the matching CLOSE_REQUEST.origin; stale replies fail |
Patch expected_revision must equal the latest event or patch-completion
revision. Rejection leaves the complete model unchanged. Conflicts require using
the newer revision and recomputing the patch; do not blindly retry stale changes.
Node IDs remain stable and accepted patches update existing controls in place.
In host 0.8.3+, multiple synchronous patch calls from one UI event callback, using
that event's panel/revision/origin, are queued in call order. Each accepted reply
provides the next call's revision; a failure cancels its dependent calls. New
user edits still conflict. Status patches can safely precede options patches.
Host 0.8.4 also supports the default origin 0 for these same-event calls.
Prefer one batch for dependent controls. Timer/completion callbacks must use a
fresh revision; see the migration guide.
Layout topology is immutable for an instance; use visible conditions or a separate
panel definition instead of injecting arbitrary UI source.
Handles include session identity, project generation, slot and generation. Closing, project replacement, disable and worker replacement invalidate the corresponding handles. Reopening creates a new handle. Events contain the handle, definition, instance, node, scalar value, revision and monotonic per-instance origin. Input previews do not change stored values or revisions. Commit/selection validates before publishing, increments the revision and sends the accepted value. Programmatic patches/binding propagation never generate user commits, avoiding feedback loops. Invalid input leaves the last valid value and an inline error; Enter commits, Escape restores, and normal text clipboard/Tab behavior comes from ET controls.
Controls and property contract
Property IDs and event types are separate domains. ET_UI_EVENT_SELECTED
(legacy ET_UI_SELECTED) is an event, never a property. Set a select's index with
ET_UI_SELECTED_INDEX / ET_UI_VALUE or the C++ selectedIndex() helpers.
See the pre.2 migration example for dynamic options.
The C header lists the exact constants. UI properties accept finite F64, BOOL
or bounded UTF-8 STRING values as appropriate. Other tagged variants are rejected.
Text is plain text; QML, HTML, JavaScript and arbitrary expressions are never
evaluated. Numeric fields use a locale-independent decimal point in this version.
| Node kinds | Rendering and value |
|---|---|
| column, row, grid, section, form | ET spacing; form fields align labels and units; grid has 1–12 columns |
| tabs, splitter, scroll | selected tab index; horizontal two-child splitter ratio 0.1–0.9; bounded nested scrolling |
| label, banner, separator | plain text and themed feedback |
| button | ordered click event on the worker dispatcher |
| text, number | string or finite value with minimum/maximum/decimals; read-only support |
| checkbox, switch | boolean value, keyboard/mouse input and ET theme |
| select | newline-delimited options and zero-based selected index |
| slider, progress | constrained number; progress is 0–1; combine buttons with jobs for Cancel |
| image | package PNG/JPG beside the manifest or under assets/, at most 2 MiB; host constrains decoded display size |
| list, table, tree | bounded data, virtualized rows, filter, selection; table sorts by column; tree expands/collapses |
| chart | at most 512 finite samples, automatically scaled and theme-aware |
Common properties: title, value, enabled, visible, tooltip, error, readOnly, help,
min/preferred/max width and height, fill and stretch. help permits HTTPS only,
opened by an explicit user click. Dimensions are logical pixels scaled by the
current ET scale. Chart/data strings remain subject to the 4096-byte string bound.
Table options supplies newline-delimited column headings; data uses newlines
for rows and tabs for cells. Tree data uses leading tabs for indentation. All
selection events report the original data row, independent of sorting/filtering.
These are plugin-owned display datasets; they grant no calibration or ECU access.
Event value types
The following are input events; programmatic patches do not emit them. All
control kinds are covered below. Lifecycle events (OPENED, CLOSED,
CLOSE_REQUEST, FOCUSED, RESIZED) carry ET_VALUE_NULL; panel/instance and
revision/origin identify their context.
| Control | Input event | et_ui_event.value |
|---|---|---|
| button | ET_UI_CLICK |
ET_VALUE_NULL |
| text | ET_UI_PREVIEW, ET_UI_COMMIT |
ET_VALUE_STRING |
| number, slider | ET_UI_PREVIEW, ET_UI_COMMIT |
ET_VALUE_F64 |
| checkbox, switch | ET_UI_COMMIT |
ET_VALUE_BOOL |
| select, tabs | ET_UI_EVENT_SELECTED |
ET_VALUE_F64, zero-based option/tab index |
| list, table, tree | ET_UI_EVENT_SELECTED |
ET_VALUE_F64, original data-row index before filtering/sorting |
| splitter | ET_UI_COMMIT |
ET_VALUE_F64, ratio 0.1–0.9 |
| plugin-owned calibration table, curve, surface | ET_UI_COMMIT |
ET_VALUE_STRING, tab/newline numeric dataset |
| channel picker, analysis panel | ET_UI_COMMIT |
ET_VALUE_STRING, chosen ID/text |
| field editor, host-backed calibration table/curve/surface | host tuning operations | Use tune/read-service events rather than generic UI input |
| column, row, grid, section, form, scroll, label, banner, separator, progress, image, chart, live panel, datalog panel | none | No generic value-input event |
Property types and control use
The host validates property types on the complete resulting model. A property irrelevant to a control may be stored but has no rendered effect; it does not change that control's value type. All properties in a patch must validate.
| Properties | Type | Control use |
|---|---|---|
| title, tooltip, error, help | STRING | Common text; help must be HTTPS |
| enabled, visible, readOnly, persist, twoWay, fill | BOOL | Common state/layout; persist opts a value into storage |
| bindValue, bindEnabled, bindVisible | STRING | Same-panel node IDs; value bindings must have compatible types |
| min/preferred/max width and height, stretch | F64 | Common layout limits; stretch is integral |
| minimum, maximum, step, decimals | F64 | Numeric controls and plugin datasets; decimals is integral 0–8 |
| units | STRING | Numeric/dataset label; never a selected index |
| options | STRING | Select options or table headings, newline delimited; empty select = no options |
| value / selected index | See event table | Corresponding control's typed value; progress F64 0–1, select allows -1 for none |
| columns | F64 | Grid/dataset column count, integral 1–12 |
| data | STRING | List/table/tree/chart or initial plugin dataset |
| source | STRING | Image: bounded adjacent PNG/JPG |
| target | STRING | Prefab host ID; empty target selects a plugin-owned dataset |
For empty select options, value=-1 is recommended; legacy default 0 is also
accepted, but no item is rendered and no user selection is emitted. Replace
options and index together to avoid retaining an out-of-range old selection.
Rejection details identify the node and property, appear in plugin diagnostics,
and are delivered as the completion's code and borrowed detail string.
bindValue, bindEnabled and bindVisible name another node in the same instance.
Conditions require boolean values. Value types and destination constraints are
checked, cycles are rejected, and one-way binding is the default. Set twoWay for
editable aliases; source read-only/disabled state cannot be bypassed. Channel and
tune sources use the read services and calibration prefabs; local UI-model bindings do not confer access.
Panels, persistence and failure
Explicit docked and popout panels
Use a placement mask to say where a definition is allowed to open. A floating panel uses the same ET controls and prefabs as a workspace panel; it needs no custom renderer or native window library.
PanelBuilder docked("com.example.tool.main", "Calibration workspace");
docked.definition.placements = ET_PLACEMENT_WORKSPACE;
docked.add(ET_UI_COLUMN, "root");
docked.prefab(ET_UI_CALIBRATION_TABLE, "table", "fuelTable", "root");
ui.define(docked, &completion);
PanelBuilder popout("com.example.tool.popout", "Calibration popout");
popout.definition.placements = ET_PLACEMENT_FLOATING;
popout.add(ET_UI_COLUMN, "root");
popout.prefab(ET_UI_CALIBRATION_TABLE, "table", "fuelTable", "root");
ui.define(popout, &completion);
// After both definitions complete successfully, open on explicit user actions:
ui.open("com.example.tool.main", {}, ET_PLACEMENT_WORKSPACE);
ui.open("com.example.tool.popout", {}, ET_PLACEMENT_FLOATING);
Declare both definitions and their placements in the signed manifest. This example
uses two singleton definitions, so each can be open and focused independently.
Route events by event.definition and keep asynchronous callback/model state
per view. Project-backed prefabs share calibration data; plugin-owned inputs and
UI revisions belong to their view. See the complete
calibration workbench.
Alternatively, one definition can allow both placements and multiple instances:
panel.definition.placements = ET_PLACEMENT_WORKSPACE | ET_PLACEMENT_FLOATING;
panel.definition.flags |= ET_PANEL_MULTIPLE;
// After successful definition:
ui.open("com.example.tool.main", "Docked", ET_PLACEMENT_WORKSPACE);
ui.open("com.example.tool.main", "Popout", ET_PLACEMENT_FLOATING);
In that case route state by event.instance and panel handle. Opening the same
instance again focuses its existing window; it does not move it to another
placement. Close then reopen explicitly to change its placement.
placements is a mask of Workspace, Floating and Modal. Placement zero chooses
saved state, then the ET pop-outs preference where permitted. Single-instance
panels use an empty instance ID; opening again focuses them. Multiple-instance
definitions require explicit IDs and own independent nodes, values and handles.
All floating/modal windows are ET-managed. Modality is parent-window modality.
The GUI never waits synchronously for a plugin callback. Closing a dirty-check
panel sends CLOSE_REQUEST; a missing answer closes it after 3000 ms. Worker
failure immediately releases modal windows and disables other controls with a
recovery message. Disable closes all windows and drains the worker.
ET_PANEL_PROJECT restricts open/restore to a current project. Closing/replacing
that project invalidates its panels. ET_PANEL_PERSIST saves state under the
plugin/panel/instance, schema and optional project identity. Individual node values
require persist=true; sensitive values should not opt in. Schema changes start
with new state. Restart restoration only occurs after explicitly loading an enabled,
compatible development plugin. Licensing eligibility will be added at M5.
Windows are clamped onto available screens at creation/screen changes, including
positions saved on a removed monitor. Placement/geometry, workspace and internal
tab selection, splitter values and last input focus are retained.
Per worker: 32 definitions, 64 live instances, 4096 total definition nodes and
4096 live nodes, depth 32, 32 pending requests, 30 UI calls/second. Persistence
retains at most 64 identities per plugin. A serialized message must fit 64 KiB
(including envelope); large definitions must use separate panels. Batch display
patches at or below 30 Hz; Qt coalesces painting and input previews are debounced.
Overflow returns ET_ERROR_LIMIT; it does not silently truncate or apply part of
a batch. Revisions/origins/slots never wrap into reusable identities.
Acceptance boundaries
Windows x64 native Qt Quick UI is the M2 acceptance target on this workstation. Compiler/OS-specific results are recorded in the repository's M2 audit. Linux GUI, real second-monitor hot-plug, and physical Pi display/input certification require their respective environments. M4 supplies calibration prefabs and host edit services; M5 supplies signed installation/licensing; M6 supplies independent custom windows. None are implied by the host-rendered M2 panels.