# EpicTuner Plugin SDK Manual

SDK version: **1.0.0-pre.3**. C ABI: **1.1**. Recommended host: **EpicTuner 0.8.5 or later**.
This is the complete documentation, public C/C++ headers, manifest schemas and
starter/cascading example source in one UTF-8 Markdown file for developers and
coding agents. It is generated from the SDK sources; online documentation may
include host clarifications made after the immutable SDK ZIP was published.

- SDK documentation: https://www.sgkembedded.com/epictuner/sdk/index.html
- SDK package: https://www.sgkembedded.com/downloads/epictuner/EpicTuner-plugin-sdk-1.0.0-pre.3.zip
- Latest manual: https://www.sgkembedded.com/epictuner/sdk/EpicTuner-SDK-Manual.md
- License: proprietary SDK; plugin development/distribution allowed, SDK redistribution prohibited.

## Current development essentials

Use public SDK headers and CMake targets for a native C++20 plugin. Ordinary
plugins run in an isolated worker and use host-rendered controls; they do not
link private application code or Qt. Declare required capabilities and features.
Keep ABI callbacks on the dispatcher and preserve callback ownership/lifetimes.

For selects, set `ET_UI_SELECTED_INDEX` (property alias of `ET_UI_VALUE`) and
handle `ET_UI_EVENT_SELECTED` (event, legacy `ET_UI_SELECTED`). Values are F64
zero-based indexes; -1 clears the selection. Empty option lists are supported.
Patch options and the selected index together. Programmatic selection does not
emit a user-input event; update dependent controls and previews explicitly.

`ET_OK` from UI calls means queued, not accepted. Inspect the completion's code
and detail. Patches are atomic. Host 0.8.4 sequences synchronous patches from one
UI event using that panel/revision and either the event origin or default origin
0. Failure cancels dependent patches; intervening user edits still conflict.
Timer/completion callbacks require a current revision. Use `Context::check` to
name startup failures. Use native MSYS2 CMake/Ninja, not MSYS Unix Makefiles.

Some chapters retain historical milestone labels. Use these current essentials,
the SDK pre.3 diagnostics guide and pre.2 migration guide and public headers for current API behavior.

## Contents
- [EpicTuner plugin SDK 1.0.0-pre.3](#manual-chapter-1)
- [EpicTuner proprietary SDK terms](#manual-chapter-2)
- [API reference](#manual-chapter-3)
- [Processes, callbacks and ownership](#manual-chapter-4)
- [Runtime contract: ABI 1.1 / wire 1 / development manifest 1](#manual-chapter-5)
- [M0 decisions and assigned follow-up questions](#manual-chapter-6)
- [Compilers, runtimes and deployment](#manual-chapter-7)
- [Example gallery](#manual-chapter-8)
- [M0 foundation contract](#manual-chapter-9)
- [Build your first plugin](#manual-chapter-10)
- [Background tasks (M1)](#manual-chapter-11)
- [Plugin entitlements and activation (SDK 1.0)](#manual-chapter-12)
- [Versioning and migration](#manual-chapter-13)
- [Frozen .etplugin package contract v1](#manual-chapter-14)
- [Host-rendered prefabs (SDK 1.0)](#manual-chapter-15)
- [Package and distribute a plugin](#manual-chapter-16)
- [Read services (SDK 1.0)](#manual-chapter-17)
- [SDK 1.0.0-pre.2 migration](#manual-chapter-18)
- [SDK 1.0.0-pre.3: actionable diagnostics](#manual-chapter-19)
- [Troubleshooting](#manual-chapter-20)
- [Calibration service (SDK 1.0, et.tune.v1)](#manual-chapter-21)
- [ET UI service v1 (SDK 0.4 / M2)](#manual-chapter-22)
- [Building and checking the SDK contract](#manual-chapter-23)
- [Website build and SGK integration](#manual-chapter-24)
- [Independent custom windows (M6)](#manual-chapter-25)
- [calibration-workbench](#manual-chapter-26)
- [Cascading dropdowns and a table preview](#manual-chapter-27)
- [Fault injection — test only](#manual-chapter-28)
- [fuel-analysis-demo](#manual-chapter-29)
- [Hello panels](#manual-chapter-30)
- [ImGui inspector](#manual-chapter-31)
- [Licensed feature demo](#manual-chapter-32)
- [C, C++ and background lifecycle](#manual-chapter-33)
- [live-telemetry](#manual-chapter-34)
- [log-analyzer](#manual-chapter-35)
- [Starter panel](#manual-chapter-36)
- [Public headers, schemas and complete examples](#manual-appendices)

---

<a id="manual-chapter-1"></a>

Source: `README.md`

## EpicTuner plugin SDK 1.0.0-pre.3

**Release status: Prerelease.** The SDK targets the 1.0 API; its release status is
separate from EpicTuner's **0.8.5** application release. The SDK is a
separate download under the [proprietary SDK terms](#manual-chapter-2).

Use host **0.8.5 or newer** for contextual UI diagnostics: named result codes,
request and panel identity, patch targets, submitted/current revisions and the
last revision change. This SDK adds `resultName`, `completionMessage`, and
`Context::check(completion, step)` helpers and updates the starter examples.
C ABI 1.1, service layouts and numeric constants remain compatible.
See [pre.3 debugging guidance](#manual-chapter-19) and the
[pre.2 dropdown migration](#manual-chapter-18).

**[Download the complete Markdown manual for coding agents](https://www.sgkembedded.com/epictuner/sdk/EpicTuner-SDK-Manual.md)** —
all guides, public API headers, schemas and starter/cascading source in one file.

Build compiled plugins for calibration, telemetry and analysis. Write C++20,
construct panels with EpicTuner controls, and ship signed packages. Ordinary
plugins need no Qt or private application headers.

**[Build your first plugin](#manual-chapter-10)**

### Find your next step

| Task | Guide |
|---|---|
| Start a standalone project | [Starter tutorial](#manual-chapter-10) |
| Browse runnable source and screenshots | [Examples](#manual-chapter-8) |
| Understand processes and callbacks | [Architecture](#manual-chapter-4) |
| Look up public types and operations | [API reference](#manual-chapter-3) |
| Build panels and tuning controls | [UI builder](#manual-chapter-22), [prefabs](#manual-chapter-15) |
| Read channels, logs and settings | [Read services](#manual-chapter-17) |
| Preview and apply calibration | [Tune service](#manual-chapter-21) |
| Run background calculations | [Jobs](#manual-chapter-11) |
| Open a native ImGui window | [Windows](#manual-chapter-25) |
| Sign, install and update a plugin | [Publishing](#manual-chapter-16) |
| Activate paid features | [Licensing](#manual-chapter-12) |
| Select compilers and runtimes | [Deployment](#manual-chapter-7) |
| Diagnose a problem | [Troubleshooting](#manual-chapter-20) |
| Upgrade a preview plugin | [Migration](#manual-chapter-13) |

### What ships

The curated export contains public C11 ABI headers, C++20 helpers, CMake targets,
a standalone starter template, eight named examples plus C/C++ lifecycle samples,
synthetic fixtures, screenshots, signing and test-catalog tools, a local issuer,
conformance tests and website sources. GLFW and Dear ImGui sources retain their
license notices. Private application code, customer data and private keys are excluded.

SDK package **1.0.0** retains **C ABI 1.1** and the ABI 1.0 prefixes. Each service
has independent version negotiation. Windows x64, Linux x64 and Linux ARM64 need
separate native binaries. See [deployment](#manual-chapter-7) for the verified
compiler/runtime boundaries and [the contract](#manual-chapter-5) for layouts.

### Website and redistribution

The documentation and proposed EpicTuner landing page build into a static site
with local search, copyable code, examples and validated links. Follow
[website integration](#manual-chapter-24) to preview or prepare it for SGK Embedded.

Plugin development and distribution, including commercial plugins, are allowed.
Redistribution of the SDK itself is prohibited. See the
[proprietary SDK terms](#manual-chapter-2). Production marketplace/issuer hosting
and embedded custom rendering remain later milestones.

---

<a id="manual-chapter-2"></a>

Source: `REDISTRIBUTION.md`

## EpicTuner proprietary SDK terms

Copyright (c) 2026 SGK Embedded. All rights reserved except as granted below.

The EpicTuner SDK is **Prerelease**, targeting the 1.0 API. SGK Embedded permits
you to download, use, copy internally, and modify the EpicTuner-authored SDK
headers, tools, templates, and examples to develop, test, and maintain plugins
for EpicTuner. You may distribute and sell your plugins, including compiled SDK
header code and adapted template/example code incorporated into your plugins.
You may distribute your own plugin source, but must direct recipients to SGK
Embedded for the SDK rather than bundling SDK headers, tools, or documentation.

You may not redistribute, mirror, sublicense, or sell the SDK itself, in whole
or in part, including modified SDK copies, except for code incorporated into
plugins as expressly permitted above. The EpicTuner application is separate and
is not licensed for redistribution by these SDK terms. No general trademark or
branding license is granted. All other rights are reserved.

The SDK is provided as is, without warranties of any kind. To the extent allowed
by applicable law, SGK Embedded is not liable for damages arising from its use.

Vendored dependencies retain their own notices and licenses:

- Dear ImGui: `vendor/imgui/LICENSE.txt`.
- GLFW: `vendor/glfw/LICENSE.md`.

These SDK restrictions do not replace or restrict rights granted by those
third-party licenses. Keep their required notices with dependency code and
binaries you redistribute. Example screenshots and synthetic fixtures contain
no customer tunes or captures.

Obtain official SDK releases and documentation from
https://www.sgkembedded.com/epictuner/downloads. Production catalog and issuer
operations remain separate from this SDK release.

---

<a id="manual-chapter-3"></a>

Source: `docs/API_REFERENCE.md`

## API reference

The public C headers are the binary contract. C++ wrappers are local convenience
types, compiled into your library. Link `EpicTuner::SDK` for C or
`EpicTuner::SDKCpp` for C++20. No Qt types appear in either public surface.

### Header and service map

| Header | Public surface | Detailed contract |
|---|---|---|
| [plugin.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/plugin.h) | Query, host/plugin tables, versions, capabilities, errors, values, handles, async options | [Foundation](#manual-chapter-9), [ABI/wire contract](#manual-chapter-5) |
| [ui.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/ui.h) | Nodes/properties, panel definitions, patches, UI events/actions | [UI](#manual-chapter-22), [prefabs](#manual-chapter-15) |
| [read.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/read.h) | Project, channels, datalog and settings request/reply tables | [Reads](#manual-chapter-17) |
| [tune.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/tune.h) | Calibration catalog, snapshots, proposals, apply, undo/redo, status and burn | [Tune](#manual-chapter-21) |
| [plugin.h jobs declarations](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/plugin.h) | Cancellation-aware background work and bounded result buffers | [Jobs](#manual-chapter-11) |
| [license.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/license.h) | Entitlement status, challenge, activation and deactivation | [Licensing](#manual-chapter-12) |
| [window.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/window.h) | Independent native-window registration and dispatcher ticks | [Windows](#manual-chapter-25) |

### C++ wrappers

| Type | Purpose |
|---|---|
| [Context, Plugin, Adapter](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/plugin.hpp) | Service discovery, capabilities, logging, exception-contained entry/lifecycle adapters |
| [PanelBuilder, Ui](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/ui.hpp) | Own construction storage, define/open/patch panels and dispatch UI callbacks |
| [Reads, ReadCallback](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/read.hpp) | Initialize bounded requests and wrap read completions |
| [Tune, TuneChange](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/tune.hpp) | Encode finite normalized-edit proposals without locale-dependent number formatting |
| [Jobs](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/jobs.hpp) | Submit and cancel worker background tasks |
| [et_license_service](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/license.h) | Negotiate the C licensing table with Context::service |
| [Window](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/sdk/window.hpp) | Register an independent worker-owned native window |

### Entry point and negotiation

Export `et_plugin_query(const et_query*, et_plugin_api*)` with the calling
convention and visibility macros supplied in `plugin.h`. C++ authors normally
use `ET_PLUGIN(MyPlugin)`. Query fills a caller-sized table and negotiates the
supported ABI minor; incompatible majors fail.

Declare capabilities in both the plugin's static bitmask and its manifest.
The host grants only those authorized for that plugin. Required named features
must be available before startup. Optional features must still be negotiated
before use. SDK package version 1.0.0 does not change C ABI 1.1.

### Service call patterns

`Context::service` obtains a sized/versioned table. Wrapper `connect` methods
perform this negotiation. Check the result before calling methods on the wrapper.
`Reads::make(operation, generation)` initializes fields including `struct_size`
and the default bounded count. Supply only the fields described by the service.

`Ui::define`, `Ui::open`, `Ui::patch` and `Ui::action` return immediate validation
or queue status and optionally deliver an `et_completion`. Read-derived services
deliver an `et_read_reply` through `ReadCallback`. Do not confuse request
acceptance with completion.

UI patch revisions and tune revisions are independent. Use the latest UI
event/completion revision for patches, and the tune catalog/snapshot revision
for edit proposals. An edit proposal also needs the definition identity,
project generation and expected stored values.

### Common errors

For every control's event type and payload, see the
[UI control event and property tables](https://www.sgkembedded.com/epictuner/sdk/docs/UI.html#event-value-types). Selects emit
`ET_VALUE_F64` zero-based indexes through `ET_UI_EVENT_SELECTED`; set their value
using `ET_UI_SELECTED_INDEX`, not the legacy event constant `ET_UI_SELECTED`.

| Result | Meaning and next action |
|---|---|
| `ET_ERROR_ARGUMENT` | Invalid type, size, reserved field or value; fix the request |
| `ET_ERROR_ABI` / `ET_ERROR_UNSUPPORTED` | Version/feature unavailable; check negotiation and host version |
| `ET_ERROR_CAPABILITY` | Missing grant; align manifest, plugin declaration and authorized action |
| `ET_ERROR_STATE` | Wrong thread or lifecycle/context state; dispatch correctly and refresh context |
| `ET_ERROR_LIMIT` | Bounded queue/data limit; reduce the request, page data or wait for completion |
| `ET_ERROR_STALE` | Expired session/project token; obtain a new handle |
| `ET_ERROR_CONFLICT` | Base revision/expected value changed; reread and recompute the preview |
| `ET_ERROR_CANCELLED` | Operation cancelled; release local ownership without replay |
| `ET_ERROR_CALLBACK` | Callback threw or violated its contract; inspect worker diagnostics |

The exact values, additional error codes and structure layouts are in
[plugin.h](https://www.sgkembedded.com/epictuner/sdk/include/epictuner/plugin.h). [Troubleshooting](#manual-chapter-20)
covers loading, dependencies and host diagnostics.

### Complete working code

Use the [starter](https://www.sgkembedded.com/epictuner/sdk/templates/panel/plugin.cpp) for lifecycle/UI,
[telemetry](https://www.sgkembedded.com/epictuner/sdk/examples/live-telemetry/telemetry.cpp) for subscriptions,
[analyzer](https://www.sgkembedded.com/epictuner/sdk/examples/log-analyzer/analyzer.cpp) for cancellable background
statistics, and [calibration helpers](https://www.sgkembedded.com/epictuner/sdk/examples/common/calibration.hpp) for
normalized proposals. These sources are built as SDK consumers in the release
gate, so they are the preferred copyable references.

---

<a id="manual-chapter-4"></a>

Source: `docs/ARCHITECTURE.md`

## Processes, callbacks and ownership

An EpicTuner plugin is a native library loaded into its own worker process.
The application owns ECU communication, calibration validation, licensing and
the panels rendered with EpicTuner controls.

### The path of a request

```text
Your C++ plugin
    ↓ C++ helpers / C ABI service tables
Plugin worker (one process per enabled plugin)
    ↓ bounded authenticated local messages
EpicTuner broker
    ↓ GUI models, read adapters, edit validation, settings and licensing
Application and its existing ECU communication worker
```

The function pointers and C++ objects remain inside the worker. IPC carries
bounded values and IDs, never pointers or raw C structure images. Each panel
from a plugin shares that plugin's worker. Worker process separation contains
crashes; it is not an OS security sandbox. A native library still has the current
user account's file and network access.

### Lifecycle

The host discovers and verifies a package, checks compatibility, then starts an
enabled plugin. The worker negotiates `et_plugin_query` and calls `start`.
Lifecycle, UI and read callbacks are serialized on its dispatcher. A plugin that
starts successfully becomes ready.

Disable, update and exit drain or cancel owned work before `stop` destroys the
instance. The host replaces the worker process for updates. It does not unload a
library with live callbacks. A crash or missed heartbeat changes the plugin to a
visible failure state, revokes subscriptions and disables its controls.
Restart is explicit; it never replays prior actions or tune writes.

### Threading

Call host services from the worker dispatcher. Use [jobs](#manual-chapter-11) for expensive
CPU work. A job receives copied inputs and a cancellation token, computes a
bounded result, and returns through a dispatcher completion. Do not call UI,
logging or read services from the job thread.

Never block a callback waiting for a dialog, request or job to finish. Deliver
the next step from its completion callback. The application GUI never waits
synchronously for plugin code.

### Data lifetimes

| Value | Lifetime |
|---|---|
| Request strings, nodes, channel arrays | Copied during the service call |
| Event/reply strings and arrays | Borrowed until the callback returns |
| Callback owner | Must remain alive until cancellation/completion has drained |
| Panel/subscription/proposal handle | Scoped to its worker session and relevant project generation |
| C++ object or allocation | Created and destroyed in its own module |

Copy a borrowed log batch before passing it to a background task. Treat handles
as opaque tokens. A project close or replacement invalidates generation-bound
work even if the newly opened project has the same name or path.

### Error handling

Service calls can fail immediately without queuing a callback. Otherwise the
terminal callback reports success, cancellation or rejection. A timeout does
not prove that a calibration commit failed; consult host status and read back
the current tune before proposing another edit.

The C++ adapters catch callback exceptions inside the module and translate them
to `ET_ERROR_CALLBACK`. C authors must provide the same no-exception boundary.
See [foundation](#manual-chapter-9), [API reference](#manual-chapter-3) and
[troubleshooting](#manual-chapter-20) for exact contracts.

---

<a id="manual-chapter-5"></a>

Source: `docs/CONTRACT.md`

## Runtime contract: ABI 1.1 / wire 1 / development manifest 1

SDK 1.0 retains the frozen M0 foundation. See [common types and lifetime rules](#manual-chapter-9),
[the package contract](#manual-chapter-14), and [assigned decisions](#manual-chapter-6).
New services need their own negotiated tables and tests. The broader product contract is PLAN-plugin-sdk.md
in the private repository; this document stands alone in the exported SDK.

### ABI and ownership

Only fixed-width integers, pointers local to the worker, explicit byte spans and
function pointers cross the C ABI. Native 64-bit pointers, maximum packing of 8,
Windows `__cdecl` and the platform C calling convention on Linux are required.
No Qt/STL objects, C++ exceptions, RTTI, compiler enums, bools or heap ownership
cross the boundary. Headers assert layouts in both C and C++:

| Structure | Size | Selected offsets |
|---|---:|---|
| et_string | 16 | data 0, size 8, reserved 12 |
| et_query | 16 | size 0, ABI 4, capabilities 8 |
| et_host_api | 40 | context 16, log 24, optional get_service 32; ABI 1.0 prefix is 32 bytes |
| et_event | 40 | operation 8, generation 16, text 24 |
| et_plugin_api | 56 | ID 16, start 32, stop 40, event 48 |

Strings are valid UTF-8, without embedded NUL, with explicit byte lengths. Empty
strings may use a null pointer. All reserved fields are zero. Inputs are borrowed
only during a call; the service copies before returning. Plugin ID is immutable
plugin-owned storage valid until worker exit. Host table is worker-owned and valid
through stop; copy `Context` to retain a reference to it, not its temporary wrapper.
No host call is legal after stop returns. Instance is plugin-owned; its stop handler
destroys it using that module's allocator. A failed start returns a null instance
and releases its own resources. The C++ adapter enforces this even on exceptions.

`et_plugin_query` receives the host's maximum ABI minor and available capabilities.
The caller supplies output capacity. A plugin rejects short mandatory prefixes or
different major versions, writes only its negotiated prefix, and reports that size.
Future fields are append-only within a major. ABI 1.0 and 1.1 negotiate to the
lower supported minor; 1.1 adds optional service discovery. Incompatible required features fail
before loading the library. Runtime identity must equal the manifest identity and
required capabilities must be a subset of grants. Context metadata is exposed by
a separately versioned table; reserved future services return UNSUPPORTED.

Lifecycle/event/completion callbacks and service calls run on the worker dispatcher.
Only background job work and its cancellation probe run on other threads.
The log proxy returns `ET_ERROR_STATE` if called from another thread. Background
task queues are implemented in M1; see [JOBS.md](#manual-chapter-11). Callbacks must return promptly; start and
normal callbacks have a 3-second supervision deadline and stop has 1 second.
Nothing executes or waits on the GUI/ECU thread. The broker destructor only kills
and reaps its own child at application teardown (bounded to 1 second).

### Capabilities, features and errors

| Capability | Bit | Feature | Implemented behavior |
|---|---:|---|---|
| log | 1 | et.log.v1 | bounded UTF-8 info/warning/error diagnostics |
| events | 2 | et.events.v1 | ordered developer message callback with operation ID |
| none | 0 | et.context.v1 | service/feature metadata and frozen limits |

Events are a foundation for later UI/project events; no project/tune access exists
yet. Unknown required features and capabilities are rejected. Unknown optional
features are ignored. Recognized features require their capability in the manifest.

| Code | Meaning |
|---:|---|
| 0 | OK (log means queued, not a durable disk write) |
| 1 | invalid argument or mandatory structure prefix |
| 2 | incompatible ABI/table/identity |
| 3 | missing capability |
| 4 | message, string, log or queue limit |
| 5 | invalid lifecycle/thread state |
| 6 | caught C++ callback exception |
| 7 | unsupported operation |
| 8 | stale handle/generation |
| 9 | cancelled on worker loss; not replayed |
| 10 | deadline exceeded |
| 11 | protocol violation |
| 12 | revision/expected-value conflict |
| 13 | not found |
| 14 | I/O failure |
| 15 | busy or already committed |

Async event replies carry the operation ID and code. Broker failures retain a
human-readable reason and cancel outstanding events. Immediate C calls return a
code; optional service discovery also fills caller-owned et_error. The common
et_completion/et_async_options contracts are frozen for later asynchronous services.

### Transport and wire encoding

Qt local sockets provide Windows named pipes and Linux Unix-domain sockets. No TCP
listener is used. See [Qt local server permissions](https://doc.qt.io/qt-6/qlocalserver.html#socketOptions-prop).
Each launch uses a fresh random endpoint/session, a 256-bit system-random token,
same-user endpoint permissions, and native peer PID validation in both directions
(Windows pipe PID APIs; Linux SO_PEERCRED including UID). The token and endpoint
arrive over the child's inherited stdin pipe, never command-line arguments or an
environment variable. No plugin library is loaded before the welcome handshake.
This prevents accidental session crossing, not hostile code under the same account.

Frame: four-byte **unsigned little-endian payload length**, followed by a UTF-8 JSON
object. C structs are never copied over the wire. A frame's payload is 1..65536 bytes.
Numeric envelope fields are exact uint32 integers. The session is a UUID string.
Sequences start at 1 independently in each direction and must increase by exactly
one. Request IDs are globally increasing for host requests; replies echo them.
Generation must be zero in this worker-global preview. Nonzero/stale generations,
session mismatches, unknown envelope/body fields and messages in the wrong state
fail the connection. Unknown optional manifest features are the only ignored fields.
JSON objects use Qt's parser (duplicate member names resolve to the last value);
signed package manifests instead forbid duplicates and use the frozen JCS subset
defined in PACKAGE.md. Production verification is M5.

Envelope keys: `wire`, `session`, `seq`, `request`, `generation`, `type`, `body`.
`request=0` means unsolicited/lifecycle; events and ping use nonzero request IDs.

| Message | Direction | Exact body |
|---|---|---|
| hello | worker -> broker | token:string, id:string |
| welcome | broker -> worker | capabilities:uint32 |
| ready | worker -> broker | empty |
| log | worker -> broker | level:1..3, text:string |
| ping / pong | broker -> worker / reply | empty |
| event | broker -> worker | type:1 (message), text:string |
| result | worker -> broker | code:0..15 |
| stop / stopped | broker -> worker / reply | empty |
| error | worker -> broker | code:1..15, detail:string (max 512 characters) |

### Limits and lifecycle

| Resource | Preview limit |
|---|---:|
| Manifest / frame payload | 64 KiB |
| ABI text / log / event | 4096 UTF-8 bytes |
| Outbound socket queue | 256 KiB per direction |
| Decoded message rate | 200 per fixed 1-second window, per direction |
| Logs | 100 per fixed 1-second window (caller sees LIMIT) |
| Pending event requests | 32 per worker |
| Startup / event / heartbeat response | 3000 ms |
| Heartbeat interval | 500 ms |
| Shutdown | 1000 ms |
| Feature lists | 32 entries each |
| Future service quotas | Nonzero frozen ceilings in FOUNDATION.md; services are unavailable until negotiated |

The decoder buffers at most two bounded frames while parsing; socket reads yield
between bounded batches. Pending events, outbound buffers and diagnostic messages
are bounded; arbitrary native stdout/stderr goes to the OS null device. No growing
native-output capture is retained. Public SDK thread queues are not implemented.
Supervision runs every 100 ms, so deadlines allow that scheduling granularity.
The five-worker 100 Hz/30-second transport benchmark records p50/p95/p99/max,
queue occupancy and explicit fault gaps. Budgets: p95 <50 ms, p99 <100 ms, max
<3000 ms. Actual telemetry/ECU-poll baseline verification remains M3.

Development lifecycle: manifest validated/compatible -> Starting -> Ready ->
Stopping -> Stopped. Any failure transitions to Failed, cancels pending operations,
and terminates only that session's worker. Manual restart waits until it has exited,
creates a new session and never replays events. Safe mode refuses all starts. Signed
verification, enabled/discovered management, a GUI recovery state, and application
startup integration are not part of this checker yet.

### Platform and dependency rules

Initial API targets: windows-x64, linux-x64 and linux-arm64. macOS is deferred.
Native modules must match the running worker's OS and architecture. There is no
Windows-to-Linux binary portability promise. Use CMake MODULE output (`.dll` on
Windows, `.so` on Linux) beside the development manifest, no path traversal or
symlink outside its directory. The worker uses restricted LoadLibraryEx search on
Windows and absolute-path RTLD_NOW/RTLD_LOCAL on Linux. Linux packaged dependencies
should use an `$ORIGIN` RPATH; native OS loaders still resolve system dependencies.
LD_PRELOAD/LD_LIBRARY_PATH/DYLD injection variables are removed from worker launch.

The standalone runtime needs Qt Core/Network/Test 6.4+ to build its own test suite;
the main EpicTuner build continues to use its existing Qt 6.11 kit. SDK author code
never links Qt. Compiler runtime dependencies remain the plugin author's packaging
responsibility. Production packaging/licensing, task dispatch, UI rendering and
real data/tune services retain their assigned later gates in DECISIONS.md; their
common contracts and identifiers are defined now.

---

<a id="manual-chapter-6"></a>

Source: `docs/DECISIONS.md`

## M0 decisions and assigned follow-up questions

M0 requires every remaining contract question to have an owner and milestone;
later service implementation and business policy are not silently treated as done.
“SDK maintainer” and “host maintainer” are project implementation roles; “product
owner” is the EpicTuner owner. No external publication is authorized by this file.

| Decision | M0 disposition | Owner / next gate |
|---|---|---|
| Targets | Windows x64, Linux x64 and Linux ARM64; macOS later, per user | Host maintainer, native device/UI gates M1–M7; macOS M10 |
| Native boundary | C11 ABI 1.1, compatible 1.0 prefixes; C++20 helpers; no Qt/STL over ABI | SDK maintainer; frozen |
| Worker and transport | One worker per enabled plugin; authenticated local sockets; framed UTF-8 JSON; native peer identity | Host maintainer; frozen, manager integration M1 |
| Lifetime/error/async syntax | Fixed types, size negotiation, handles, owned errors, borrowed values/completions, committed-operation boundary | SDK maintainer; frozen in FOUNDATION.md |
| UI/service version policy | Named `.v1` features and independently versioned tables; break only with opt-in new major; unknown required features fail | SDK maintainer; frozen |
| Concrete UI properties/prefab tables | Implement through the frozen common handle/value/async contracts; do not expose private Qt controllers | UI maintainer; M2 service v1 in UI.md; full prefab library M4 |
| Background tasks | Serialized callbacks; bounded thread-safe posting, cancellation and stop draining required | Host maintainer, M1 |
| Project/tune revisions | Project generations and expected base revision/old values are mandatory; local/RAM/burn outcomes stay distinct | Project generations/read services complete in M3 (READ.md); tune revisions/edits M4 |
| Package naming/metadata/signing bytes | `.etplugin`, complete v1 schemas, Ed25519 over domain-separated JCS manifest, file SHA-256 inventory | Package maintainer; frozen; native verifier/install implemented M5 |
| Native crypto library | OpenSSL Ed25519; Windows preview stages its runtime DLL and upstream license, Linux links system OpenSSL | Package maintainer; M5 implementation, deployment versions remain platform certification work |
| Publisher trust/key revocation | Explicit per-user public-key import, no built-in roots, key-ID rotation through updated packages, local revocation blocks new and cached package starts | Package maintainer; M5 implemented. Production root distribution and remote revocation operations are product-owner M9 work |
| SDK redistribution terms | Product owner requested a prepared release with terms pending; no new redistribution rights granted here | Product owner, before external publication |
| License binding/offline grace | Signed installation-bound responses; perpetual works offline, trial stops at expiry, subscription uses signed lease and 72-hour grace with expiry as a hard stop | Licensing maintainer; M5 implemented. Refund, transfer, activation limits, recovery and server-side revocation are product-owner M9 policy/operations |
| Performance | Five-worker/100 Hz/30-second transport gate, explicit gaps/cancellations and fixed percentile/queue/deadline budgets | Host maintainer; M0 measurement, M3 actual mock polling/recording baseline in PLUGIN-SDK-M3 audit |
| Raspberry Pi acceptance | ARM64 ABI/SDK builds and emulator conformance in M0; physical device worker/UI/rendering separately verified | Host/UI maintainers, M1/M2/M6/M7 |
| ImGui backend | Windows and Linux required; Pi renderer/device requirements explicitly tested | UI maintainer, M6 |
| Embedded rendering | Copied frames baseline; choose GPU sharing only after measurement | UI maintainer, M8 |
| Commerce provider/hosting | External sales first; provider/payout/production keys not needed for SDK contracts | Product owner, M9 |

SDK constants for future capabilities or quotas do not enable those services. New
methods must use the frozen ownership/version rules and add acceptance tests before
their feature is advertised. Physical-device validation cannot be inferred from
cross-compilation or emulation.

---

<a id="manual-chapter-7"></a>

Source: `docs/DEPLOYMENT.md`

## Compilers, runtimes and deployment

Compile a distinct native library for each target. The C ABI crosses compiler
boundaries; operating-system libraries and runtime requirements still apply.
The SDK does not bundle a compiler or an EpicTuner application.

### Target matrix

| Target | Author toolchain | Evidence boundary |
|---|---|---|
| Windows x64 | MinGW GCC 13.1; x64 MSVC 19.51 | Native worker and application examples; SDK-only consumer builds against the MinGW host |
| Linux x64 | GCC 15.2 under WSL; C11/C++20 ABI | Native Linux worker/conformance; independent X11/OpenGL window under WSLg |
| Linux ARM64 | AArch64 GCC with matching sysroot | Cross-build and QEMU ABI conformance; physical Pi worker/GUI certification pending |
| macOS / Windows ARM64 / Linux armhf | Not released targets | No compatible-binary claim |

This records the environments used by the SDK milestone sequence. The M07
release audit records fresh build/test results. No physical ECU, engine, mixed-DPI
hot-plug or Raspberry Pi runtime acceptance is implied by an emulator test.

### Build requirements

Use CMake 3.24+, C11 for C authors and C++20 for wrapper consumers. Ordinary ET
panels link only the header-only SDK target and the compiler's native runtime.
The ImGui example additionally compiles its vendored ImGui/GLFW sources and links
OpenGL. Linux uses GLFW's X11 backend and requires X11, Xrandr, Xinerama, Xcursor,
Xi and OpenGL development headers. Wayland-native rendering is not certified.

Use separate output directories for compilers, build configurations and targets.
A 64-bit OS does not guarantee that the selected compiler produces 64-bit code.
Check architecture explicitly when a package reports an incompatible native file.

### Windows runtime deployment

Use the supplied host runtime bundle. It stages Qt, the MinGW runtime and the
host's OpenSSL library beside the executables. Plugin authors do not link Qt.
MinGW plugins may require `libstdc++-6.dll`, `libgcc_s_seh-1.dll` and
`libwinpthread-1.dll`; the tested host bundle already carries its matching versions.
Declare additional plugin dependencies in the signed package.

MSVC plugins need a compatible Visual C++ runtime when built with dynamic CRT.
Do not assume a compiler installation exists on the destination computer.
Verify with Qt/compiler directories removed from PATH. Export only
`et_plugin_query` and avoid passing STL objects or allocations across the ABI.

### Linux runtime floors

Build against the oldest distribution you intend to support and inspect the
resulting ELF `NEEDED` libraries and `GLIBC_*` / `GLIBCXX_*` symbol versions.
The required floor is the maximum version imported by the actual release
payload, not the compiler's marketing version. Record those imports in the
release evidence. A WSL run proves that environment; it does not certify every
Linux distribution.

The M7 GCC 15.2 x64 example build imports up to `GLIBC_2.38` for the standard
panels and `GLIBCXX_3.4.31`. Its ImGui binary imports `GLIBC_2.43` as well as
`libOpenGL.so.0`; it is a test binary for that build environment. Rebuild against
your deployment sysroot before targeting older distributions. The SDK source
does not itself impose those binary runtime floors.

The standalone worker implementation can build with Qt Core/Network/Test 6.4+.
The full application currently builds with Qt 6.11. The worker's Qt dependency
does not become an SDK author dependency. OpenGL 3.3 and an available X11 display
are required for the supplied ImGui renderer.

### Reproducible consumer verification

```sh
cmake -S /absolute/sdk/tests -B consumer -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build consumer
ctest --test-dir consumer --output-on-failure
cmake -S /absolute/sdk/tests/c-only -B c-only -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build c-only
ctest --test-dir c-only --output-on-failure
```

For ARM64, configure a matching AArch64 toolchain and sysroot and supply
`CMAKE_CROSSCOMPILING_EMULATOR` for QEMU. Run the actual worker on a physical
target before claiming device support. See [verification](#manual-chapter-23).

---

<a id="manual-chapter-8"></a>

Source: `docs/EXAMPLES.md`

## Example gallery

All examples use the exported SDK. They do not include private application
headers. The standard panels require no Qt; the independent ImGui example adds
GLFW and OpenGL. Use a disposable project and the supplied synthetic fixtures.

### Build the examples

From the extracted SDK, configure the consumer suite:

```sh
cmake -S tests -B consumer -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/absolute/sdk -DET_SDK_BUILD_IMGUI=ON
cmake --build consumer
ctest --test-dir consumer --output-on-failure
```

Each example also has its own CMake project and preset. Follow that example's
README: existing service examples use `release`; the starter and basic examples
use `sdk` with `EPICTUNER_SDK_ROOT`. Run `cmake --list-presets` in the example
directory to see its configure presets.
Keep the extracted SDK directory tree intact: some examples share helpers or
vendored sources. See [deployment](#manual-chapter-7) for compiler and renderer dependencies.

### Examples

| Example | Learn | Acceptance you can reproduce |
|---|---|---|
| [Hello panels](#manual-chapter-30) | Controls, bindings, workspace/floating/modal panels, multiple instances | Edit values; focus the singleton; open independent inspectors; close/reopen and restore state |
| [Cascading selects](#manual-chapter-27) | Dynamic Make/Model/Tune options, empty controls, typed selections and 2 × 12 preview | Choose each level; verify preview; change Make and verify downstream reset; host 0.8.3+ |
| [Live telemetry](#manual-chapter-34) | Channel selection, timestamps/quality, bounded plot, derived RPM/2 | Connect Mock ECU; change channels; disconnect/reconnect; observe stale values and gaps |
| [Calibration workbench](#manual-chapter-26) | Real field/table/curve/surface prefabs, normalized previews and undo | Use synthetic INI; preview/apply; edit manually to invalidate an old proposal; save/reopen; verify RAM separately from burn |
| [Log analyzer](#manual-chapter-35) | Ranges, background statistics, result table/chart, comparison | The supplied 256-row logs yield means 127.5 and 227.5; cancel and rerun |
| [Fuel analysis](#manual-chapter-29) | Bounded correction calculation and explicit Apply | Analyze synthetic lambda log; change limits; reject stale previews and absent channels; no automatic burn |
| [Licensed feature](#manual-chapter-32) | Signed package, free/paid action, activation | Paid action denied before activation; accepted after valid mock online/offline activation |
| [ImGui inspector](#manual-chapter-31) | Worker-owned custom window and host reads | Render/input, focus, resize, close/reopen, worker-loss recovery |
| [Fault injection](#manual-chapter-28) | Intentional worker failure and invalid service calls | Host survives crash/hang; flood is bounded; invalid requests fail; restart is explicit |

### Install through the signed test catalog

The catalog builder takes binaries from that independent consumer build and
signs all eight packages with an ephemeral local test key:

```sh
python tools/test-catalog.py build --sdk /absolute/sdk --binaries /absolute/consumer --output /absolute/test-catalog
python tools/test-catalog.py verify --catalog /absolute/test-catalog --trusted-key /absolute/test-catalog/publisher.json
```

The private key is deleted when the builder exits. The catalog contains public
trust material, signed packages and hashes. The fault-injection entry is
explicitly marked test-only. Review/import the public test publisher in the
Plugins manager, then install selected `.etplugin` files. Never import this
generated identity as a production publisher. Catalog verification does not
replace the host's independent package verification.

See [publishing](#manual-chapter-16) for the package layout and third-party path.
The small [lifecycle examples](#manual-chapter-33) additionally
show C, C++ and background-task ABI fundamentals.

---

<a id="manual-chapter-9"></a>

Source: `docs/FOUNDATION.md`

## M0 foundation contract

SDK 1.0 retains the ABI **1.1** foundation frozen in M0. The original ABI 1.0 prefixes remain binary compatible.
These definitions are frozen for the initial Windows x64, Linux x64 and Linux ARM64
targets. Breaking layouts/semantics require an ABI or named feature major change.
macOS is a later certification target. This contract does not advertise later
services as implemented merely because their identifiers are reserved.

### Query, tables and caller-owned storage

`et_plugin_query` takes the largest ABI minor the host supports and the granted
capability mask. Return the lower supported minor within the same major. Reject a
different major, missing required capability, or insufficient mandatory prefix.
An error leaves the output table unchanged. A success writes only the negotiated
prefix and reports its actual size. Extra caller capacity remains untouched.

`et_plugin_api` stays 56 bytes. `et_host_api` retains its original 32-byte prefix;
ABI 1.1 appends `get_service` at offset 32, total 40 bytes. Check `struct_size` before
reading that pointer. An ABI 1.0 host has no service discovery and the C++ wrapper
returns `ET_ERROR_UNSUPPORTED` without reading past its prefix. A newer host can
run an older plugin. Append-only service tables negotiate versions independently.

`get_service(context, id, requestedVersion, table, capacity, error)` copies a table
into caller-owned storage; it never returns heap ownership. The context table v1
is 32 bytes and provides limit and named-feature queries. Unknown/unimplemented
services return UNSUPPORTED; incompatible service majors return ABI; insufficient
output capacity returns ARGUMENT. Optional `et_error` must have a valid size prefix.
Its fixed 512-byte detail buffer belongs to the caller, is NUL terminated, and
reports byte length excluding the terminator. Immediate calls use operation ID 0.
Too-short error/output prefixes are never overwritten.

Current features: `et.context.v1`, `et.log.v1`, `et.events.v1`. Feature availability
and capability grants are separate: metadata can describe an implemented feature
whose capability was not granted. An unavailable future service is not returned as
an empty success table. See `contracts.h` for stable service IDs 1–11 and capability
bits 0–14. Reserved capability names are catalogued in the package schema. Only
log/events are granted by this development runtime; context metadata needs no grant.

### Values and handle lifetime

`et_value` is a 40-byte tagged union: null, bool (0 or 1), signed/unsigned 64-bit
integer, finite binary64, UTF-8 string, bytes, or handle. Reserved fields are zero.
Inactive union storage is ignored and never serialized. All pointed-to storage is
borrowed only during the call/callback. Text rejects NUL, malformed/overlong UTF-8,
surrogates and out-of-range scalars. Bytes may contain arbitrary data. Use the SDK
validators before enqueueing. Copy inputs before returning from any submit call;
copy completion data before returning from its callback. No module frees another
module's allocation, including error messages and output tables.

`et_handle` is 32 bytes: opaque session tag, project generation (0 means global),
slot, slot generation, kind and reserved. Slot/session/generation 0 is invalid.
Handles confer no authority unless the host registry resolves the full tuple in
the owning session and required scope/kind. Closing a panel invalidates its nodes
and panel-scoped resources; closing a project invalidates its scoped handles.
Global handles survive project changes, but no handle survives worker replacement.
Reusing a slot increments generation; exhausted generations retire the slot rather
than wrap. Stale/project/kind/session mismatches return STALE without dereferencing
any plugin-supplied pointer. The host reference registry and tests exercise these
rules; future service adapters must use equivalent checks at dispatch.

UI placement flags are workspace=1, floating=2, modal=4. Every panel instance has
independent handles; single-instance reopening resolves/focuses the existing valid
instance. Node/property schemas and concrete prefab service tables belong to named
`et.ui.v1` feature work in M2/M4; raw QML injection is never a fallback. This freezes
the common identifiers/lifetimes without claiming a working renderer.

### Async syntax, ordering and cancellation

Future asynchronous service submissions use a caller-owned `et_async_options`:

```cpp
et_async_options options{};
options.struct_size = sizeof(options);
options.timeout_ms = 1000;
options.context = myPluginState;
options.complete = [](void* state, const et_completion* result) {
    // Inspect result->code/operation_id; copy any needed value/detail now.
    // Callback and its data belong to the worker dispatcher for this call only.
};
// A service's submit(..., &options, &operationId) returns a synchronous et_result.
// Concrete submit functions are supplied only by implemented, negotiated services.
```

Accepted submission returns OK and a nonzero operation ID; rejected submission
returns an error, operation ID 0, and schedules no callback. IDs monotonically
increase without reuse in a session. Local callback pointers/context never cross
IPC: the worker stores them and routes wire completions by session/operation ID.
Successful submission has one terminal completion while the worker remains alive;
stop revokes its callback registrations after draining/cancelling unsent work.
Worker death cannot deliver callbacks into dead code; the host still retains the
operation outcome for diagnostics/reconciliation and never replays side effects.

Operation phases are queued -> running -> committed -> complete. “Committed” means
an irreversible host operation has been dispatched, not that an ECU flash burn
completed. Cancellation before that boundary yields CANCELLED exactly once. After
it, cancellation returns BUSY and the host preserves the real outcome. Deadlines
use monotonic host time, 1..3000 ms, and cancel undispatched/ordinary work with
TIMEOUT. Project change yields STALE for unfinished pre-commit work. Already-sent
host writes retain their outcome through cancellation, expiry and worker loss;
M4 implements the ECU readback/partial-write adapter. Completion acknowledgement
releases ledger capacity. Unknown, duplicate or replayed IDs never repeat a write.

`et_completion` is 80 bytes with size, code, operation ID, project generation,
tagged value and borrowed detail. Failed completions use a null value. Numeric
64-bit IDs/values use canonical decimal strings on future JSON service bodies to
avoid precision loss; existing worker wire correlation IDs remain uint32. Never
serialize these native structs, padding, pointers or callbacks verbatim.

### Limits and execution policy

`contracts.h` is the numeric authority and the context service reports it. These
are contract ceilings, not grants: a service unavailable today still cannot be
used merely because a future limit is nonzero.

| Resource | Per-worker ceiling |
|---|---:|
| Wire frame / outgoing queue | 64 KiB / 256 KiB |
| Pending operations | 32, including unacknowledged terminal outcomes |
| Text / structured error detail | 4096 / 511 UTF-8 bytes plus error terminator |
| UI nodes / nesting | 4096 / 32 |
| Panel definitions / instances | 32 / 64 |
| Subscriptions / background jobs | 64 / 16 |
| UI updates / data source | 30 Hz / 100 Hz |
| Logs / wire messages | 100 / 200 per fixed second |
| Startup / ordinary operation | 3000 ms / 3000 ms |
| Heartbeat interval / response | 500 ms / 3000 ms |
| Shutdown / supervisor resolution | 1000 ms / 100 ms |

Future display streams coalesce at 30 Hz and report losses; acquisition batches
preserve source timestamps/gap counters. M0 validates transport/state contracts;
M3 validates real channel/recording adapters against a matched ECU-poll baseline.
No callback runs on or synchronously blocks the application GUI/ECU thread.
Service calls require the dispatcher. M1 background tasks publish through bounded
thread-safe result mailboxes; submission rejects at capacity. See [JOBS.md](#manual-chapter-11).

Performance gate: five isolated test workers, 100 Hz timestamped synthetic source,
30 seconds, one crash/manual restart, no completion replay, queue occupancy <=32,
explicit cancellation/gap counts, p95 <50 ms, p99 <100 ms and maximum <3000 ms.
Startup/shutdown must retain their stated deadlines. This is a transport benchmark,
not endurance, UI rendering, ECU polling or physical Raspberry Pi proof.

### Full lifecycle contract

| State | Allowed next state and condition |
|---|---|
| Discovered | Verified after package authenticity policy; Failed/Disabled on rejection |
| Verified | Compatible after target/host/ABI/required-feature checks |
| Compatible | Enabled after user grant/license policy; Disabled otherwise |
| Enabled | Starting on explicit activation, or Disabled |
| Starting | Ready after authenticated handshake + successful start; Failed on deadline/error |
| Ready | Stopping on disable/update/close; Failed on crash/protocol/heartbeat error |
| Stopping | Stopped after both stop acknowledgement and normal process exit; Failed on deadline/error |
| Stopped | Starting only after renewed compatibility/grant checks, or Disabled |
| Failed | Disabled or explicit revalidation/restart; never automatic write replay |
| Disabled | Verified/Compatible/Enabled only after explicit re-enable and renewed checks |

The development checker explicitly substitutes unsigned development opt-in for
production verification and does not claim a verified signature. Safe mode prevents
all starts. The GUI manager and production installer implement the preceding policy
states in M1/M5; the runtime skeleton enforces starting through stopped/failed.

---

<a id="manual-chapter-10"></a>

Source: `docs/GETTING_STARTED.md`

## Build your first plugin

This tutorial creates a compiled C++ panel with a button, an editable name and
persistent panel state. It uses the public SDK and runs in the EpicTuner worker.

### 1. Prepare your tools

Extract the SDK into a new directory. Install CMake 3.24 or newer, Ninja, and a
native C++20 compiler. Use an **x64** compiler shell on Windows. MinGW and MSVC
produce separate binaries; both use the same C ABI. Python is needed for the
generator and package tools, not for running a plugin.

You also need an EpicTuner host build containing `epictuner`,
`epictuner-plugin-host` and `epictuner-plugin-check`. Windows executable names
end in `.exe`. The host ships separately from the SDK.

### 2. Generate the project

Run from the extracted SDK root:

```sh
python tools/new-plugin.py /absolute/my-plugin --id com.example.analysis --name "My analysis"
```

The generator writes a standalone CMake project, a development manifest and
`plugin.cpp`. It refuses to overwrite an existing directory. Choose a reverse
domain ID you control; the library and manifest must agree.

### 3. Compile

```sh
cmake -S /absolute/my-plugin -B /absolute/my-plugin/out -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build /absolute/my-plugin/out
```

For MSVC, run in an initialized x64 developer shell and add
`-DCMAKE_CXX_COMPILER=cl`. For MinGW, select `g++` or its absolute path.
Use a fresh output directory when changing compilers.

For MSYS2, use the **UCRT64** or **MINGW64** shell with its matching native CMake,
Ninja and compiler packages, for example `mingw-w64-ucrt-x86_64-cmake`,
`mingw-w64-ucrt-x86_64-ninja`, and `mingw-w64-ucrt-x86_64-gcc`. `which cmake`
should point into `/ucrt64/bin` (or `/mingw64/bin`), not `/usr/bin`.
Always specify `-G Ninja`. MSYS `/usr/bin/cmake` plus Unix Makefiles and native
`mingw32-make` mixes `/c/` paths with Windows paths and does not form a supported
toolchain. The SDK CMake project is the recommended build path.

Alternatively set `EPICTUNER_SDK_ROOT`, change into the generated directory,
and run `cmake --preset sdk` followed by `cmake --build --preset sdk`.
The result is `starter.dll` on Windows or `starter.so` on Linux, beside
`plugin.json`. The CMake helper also places the manifest beside multi-config
output such as `Release/starter.dll`.

### 4. Check the worker

Run the checker from your host installation:

```sh
epictuner-plugin-check --development --manifest /absolute/my-plugin/out/plugin.json --ui-panel com.example.analysis.main
```

Expect `READY` and `UI registration/open/focus/close passed`. This verifies
loading and panel registration; the next step verifies actual rendering and input.
A missing native dependency is diagnosed before the panel can register.

### 5. Open it in EpicTuner

```sh
epictuner --plugin-development --plugin-manifest /absolute/my-plugin/out/plugin.json
```

Open **Plugins → Examples → My analysis**, then click **Say hello**. The status
changes to “Hello from your compiled plugin!” Edit Name, close the panel and
reopen it. The name is opt-in persistent state. The panel supports workspace and
floating placement. Plugin diagnostics are available through **Tools → Plugins**.

Unsigned development manifests require these launch flags every time.
`--safe-mode` suppresses plugin startup, including development plugins.

Alternatively launch with only `--plugin-development`, open **Tools > Plugins**,
and choose **Load development manifest…**. This loads the plugin for this launch;
unsigned development registration is intentionally not persisted. Repeat the load
after restarting, or use the CLI manifest argument on each launch. The supported
CLI form is two arguments: `--plugin-manifest /absolute/path/plugin.json`.

### How the code works

The complete, buildable source is
[the panel template](https://www.sgkembedded.com/epictuner/sdk/templates/panel/plugin.cpp). It subclasses
`et::sdk::Plugin`, declares its identity/capabilities, and exports the
`et_plugin_query` entry point through `ET_PLUGIN(Starter)`.

`start` negotiates `Ui`, builds a `PanelBuilder`, and defines it. The host creates
the real controls. Click events run on the worker dispatcher. The handler sends
an atomic `Ui::patch` using the event's revision and origin, and receives a
completion result. A returned `ET_OK` means the request was accepted for
processing; check the completion for the final outcome.

The template keeps its callback owner and completion options in the plugin object.
The builder owns its temporary strings; service calls copy input data before
returning. Never retain pointers into a borrowed event or reply.

### Add your workflow

Use [read services](#manual-chapter-17) for telemetry and logs, [jobs](#manual-chapter-11) for background
calculation, and [calibration proposals](#manual-chapter-21) to preview edits before an
explicit Apply. Try the [examples](#manual-chapter-8) with their synthetic fixtures.
Package your result using the [publishing guide](#manual-chapter-16).

---

<a id="manual-chapter-11"></a>

Source: `docs/JOBS.md`

## Background tasks (M1)

Declare `jobs` and require `et.jobs.v1`, then query `ET_SERVICE_JOBS` version
`ET_SERVICE_VERSION_1`. This optional 32-byte table uses ABI 1.1 discovery.
Existing 1.0 plugins remain compatible. The Qt-free C++ wrapper is
`<epictuner/sdk/jobs.hpp>`; `background.cpp` shows the C callbacks and result data.

Call submit/cancel on the worker dispatcher. Work runs on a background thread.
At most 16 jobs are outstanding; excess submission returns ET_ERROR_LIMIT without
blocking. Each job has a 4096-byte result mailbox, published with atomic
release/acquire synchronization. Only the cancellation probe is callable from
work; other services, including logging, require the dispatcher. Return data
and log it in completion. No native pointer or callback travels over IPC.

The plugin owns work_context and options.context through completion. Submit copies
options, issues a worker-global job handle, and never calls completion inline.
Work writes into the supplied buffer, sets size and returns an et_result; do not
change its pointer/capacity. Completion receives borrowed ET_VALUE_BYTES on success
or a null value on error. Its operation ID equals the job handle slot. Copy retained
bytes before returning. Author callbacks must contain exceptions for cross-compiler
use; the worker also catches compatible unexpected exceptions as containment.

Cancellation is cooperative. Cancel signals the atomic probe; timeout does the
same after timeout_ms (1–3000 ms). The first cancellation reason wins. Completion
runs exactly once AFTER work returns, so the plugin can safely release contexts.
Work ignoring cancellation for another 1000 ms fails and terminates the worker;
callbacks cannot be promised after process failure. The broker also enforces its
independent shutdown deadline. Stop rejects new jobs, cancels/drains outstanding
work and completions, THEN invokes plugin.stop and destroys the instance.
Completion callbacks must return promptly, like other dispatcher callbacks.

Completed, forged, wrong-kind/session and project-scoped job handles return
ET_ERROR_STALE. Slots never reuse within a worker; new random sessions invalidate
prior handles. Results and work never replay on restart. Jobs are worker-global
CPU work; project services, progress UI and ECU work belong to later milestones.

Build all three examples with examples/lifecycle/CMakeLists.txt. Run:

```
epictuner-plugin-check --development --manifest out/plugin-background.json
```

Expected log: background sum=55, then stop after completion. In EpicTuner, launch
with --plugin-development and use Tools → Development Plugins → Load development
manifest. --safe-mode overrides development mode and refuses all starts.
Development enablement/manifests are per-launch; unsigned plugins never auto-load
from previous sessions. Signed installation, trust, persistent enablement and
update/rollback belong to M5.

---

<a id="manual-chapter-12"></a>

Source: `docs/LICENSE.md`

## Plugin entitlements and activation (SDK 1.0)

Package authenticity and purchase rights are separate. A `.etplugin` is accepted
only when its Ed25519 publisher key is trusted and its inventory verifies. A
paid product additionally needs a signed entitlement from a separately trusted
issuer. The host contains no private keys and trusts no test issuer by default.

### Service contract

Request `ET_SERVICE_LICENSE` v1 with an `et_license_service` table from
`<epictuner/license.h>`. It uses the same bounded async request/reply structure
as the read services. All calls run on the worker dispatcher; callbacks arrive
on that dispatcher and are cancelled during stop. Operations:

| Operation | Reply |
|---|---|
| `ET_LICENSE_STATUS` | `reply.text` is compact JSON with `state`, `allowed`, `reason`, and `features`. `request.id` may name one feature. |
| `ET_LICENSE_CHALLENGE` | `reply.text` is the current offline challenge JSON. A new challenge replaces the previous one. |
| `ET_LICENSE_ACTIVATE` | Opens the host Plugins window so the user can complete online or offline activation. |
| `ET_LICENSE_DEACTIVATE` | Clears the cached local entitlement. |

For the `licensed-feature-demo`, `ET_LICENSE_STATUS` checks
`org.epictuner.feature.pro` before running the paid action. Host tune proposal,
apply, undo, redo and Burn requests from a paid plugin are checked again at
dispatch; completed host operations are not undone when a license expires.
Read-only work and panel close/export remain available. Plugin authors must
check their own protected algorithms at the point of use as well.

### Signed response

The issuer signs canonical UTF-8 JSON entitlement bytes with Ed25519 over
`EpicTunerEntitlement/v1\n` followed by the canonical entitlement. The response
contains the original challenge, entitlement and detached signature. The
entitlement binds issuer/key ID, product/plugin, license ID, policy, features,
version range, installation ID, challenge nonce, issue time, expiry and lease.
Only the public issuer key is imported into the host. The issuer private key
remains outside the SDK, host, package and cache.

The plugin manager supports HTTPS activation endpoints and a loopback HTTP
exception for the local test issuer. Offline activation exports a challenge
JSON file and imports a response JSON file signed for that challenge. A
response for another product, installation, nonce or issuer is rejected.
Each new activation requires a new challenge. The manager reports failures
without unloading the plugin or replaying work.

### Policy

- **Free:** no entitlement or network access.
- **Trial:** signed expiry; no offline extension after expiry.
- **Perpetual:** version-bounded, available offline indefinitely while the
  issuer key remains trusted and the installation ID matches.
- **Subscription:** signed expiry is a hard stop. A signed `leaseUntil` permits
  72 hours of offline grace before new restricted work is blocked. The issuer
  must renew the lease through a new activation before grace ends.

The host rejects a clock more than five minutes behind its last observed time.
That local check cannot prevent a user who controls their machine from changing
state or rolling back storage. Revoked issuer keys block cached receipts on the
next status check; offline clients cannot learn server-side revocations until
they reconnect or their signed lease/grace ends. Publishers must define refund,
transfer, activation-limit and account-recovery policy in their issuer service.

Windows caches the signed response with DPAPI for the current user. Linux stores
the signed response in a `0600` per-user file. Neither format is a claim of
tamper-proof DRM; user-controlled native code and local account access remain
outside the plugin boundary.

### Local test issuer

`tools/test-issuer.py` is deliberately local and uses an **external** Ed25519
PEM. Test keys are not shipped in the SDK or application. Example:

```sh
python tools/test-issuer.py issue --key /outside/sdk/issuer.pem \
  --issuer org.example.test --key-id org.example.test.key1 \
  --policy subscription --feature org.epictuner.feature.pro \
  --expires 1790200000 --lease 1790120000 \
  --challenge challenge.json --output response.json
```

For online mock activation, replace `issue` with `serve --port 8765` and point
the Plugins window to `http://127.0.0.1:8765/activate`. Production activation
requires an independently deployed HTTPS issuer, revocation and recovery
operations. The mock proves the host/SDK mechanics only.

---

<a id="manual-chapter-13"></a>

Source: `docs/MIGRATION.md`

## Versioning and migration

SDK 1.0.0 retains C ABI 1.1 and ABI 1.0 structure prefixes. Service tables keep
their independent v1 negotiation. Updating the SDK package version alone does
not change the wire schema or entitlement format.

### From the 0.x SDK previews

Update `find_package(EpicTunerSDK 0.x REQUIRED CONFIG)` to
`find_package(EpicTunerSDK 1 REQUIRED CONFIG)` and use a fresh build directory.
CMake package compatibility follows SDK major versions; a 0.x version request
does not select a 1.x package automatically.

Rebuild each OS/architecture library independently. Recheck required features
and manifest capabilities. Run conformance tests, exercise your plugin in a
staged application, then sign a new immutable package version. Do not replace
files under a running worker.

The Hello Panels image now uses `assets/epictuner.png` so the same code works in
both development layout and signed package inventory. Copy that directory beside
the development manifest.

### Settings migration

Settings carry a schema version and are namespaced to the plugin and optional
project. Read the existing schema before writing an upgraded representation.
Preserve data you can migrate, report invalid settings, and use deterministic
defaults only for genuinely absent values. Test rollback with the previous
plugin version; a newer schema may require a reversible representation.

Window restoration is separate from settings migration. Restoring placement
must never simulate a prior button click or replay an edit.

### Porting a TunerStudio Java plugin

There is no JAR, Swing or Java binary compatibility. Port calculations and data
models to C++ and replace the UI with ET controls or supported prefabs.

| Java/Swing concept | EpicTuner counterpart |
|---|---|
| Application plugin startup/shutdown | `Plugin::start` / `stop` in an isolated worker |
| Returned Swing panel | Registered `PanelBuilder` definition |
| Event dispatch thread work | Worker dispatcher callback plus host-owned UI updates |
| Long-running analysis thread | SDK job with cancellation and dispatcher completion |
| Raw calibration mutation | Host-validated proposal, explicit Apply and readback |
| Application-specific classes | Negotiated C ABI service tables |

Avoid translating blocking dialogs or direct memory access literally. Design
each step as an asynchronous request/completion and retain only owned copies of
borrowed data.

---

<a id="manual-chapter-14"></a>

Source: `docs/PACKAGE.md`

## Frozen .etplugin package contract v1

M0 froze naming, metadata, encoding, limits and test vectors. M5 implements
native archive verification, per-user signed installation, rollback and
entitlement enforcement. A structurally valid manifest or test signature does
not authorize loading; a trusted publisher public key is required.

`.etplugin` is a ZIP archive with UTF-8 names, stored/deflated regular files only.
Reject encryption, symlinks/hardlinks/special files, duplicate or case-colliding
paths, directory traversal, absolute paths, backslashes, drive/stream separators,
Windows device names and trailing spaces/dots. Directory entries are optional and
carry no payload; directories are otherwise implicit. ZIP64 is unnecessary and
rejected in v1. Limits: 128 MiB archive, 256 MiB total expanded, 64 MiB per file,
4096 payload files, 240 ASCII bytes per portable path, maximum 8 components, and
100:1 maximum per-file expansion ratio. Enforce bounds while streaming, not after
unbounded allocation or extraction. Manifest/signature JSON are each <=64 KiB.

```text
plugin.json
signature.json
payload/windows-x64/plugin.dll
payload/linux-x64/plugin.so
payload/linux-arm64/plugin.so
assets/...
licenses/...
```

`package-manifest.schema.json` requires stable plugin/publisher/key IDs, display
name and description, strict three-component version, HTTPS help/support/home URLs,
host minimum/inclusive and maximum/exclusive range, ABI range, required/optional
feature lists, capabilities, per-target entries, panel/service declarations,
settings schema, complete file inventory and dependency/license inventory. Product/
entitlement metadata is optional and separate from package authenticity. Each
target has exactly one declared entry. Executable dependencies must be individually
inventoried under their target payload directory. Every other payload is an asset
or license; extra/unlisted payloads are rejected. Every file lists SHA-256, exact
expanded byte length, role and platform. No self-referential inventory of
`plugin.json` or `signature.json` is allowed. Dependency and panel namespaces must
agree with declarations; runtime panel registrations must agree with the manifest.

Manifest schema version and ABI/service/wire versions are independent. Unknown
manifest keys are rejected. Unknown optional features may be ignored; unknown
required features or capabilities are not granted. Native libraries match the
selected platform (including ABI architecture), not merely their filename suffix.

### Signing bytes

The detached signature uses Ed25519 as specified by [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032).
Validate the complete manifest schema before canonicalization. The JSON subset
forbids duplicate keys, invalid Unicode and floating-point numbers; numeric metadata
uses safe-range integers. Canonical bytes follow [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785),
with no whitespace, sorted object keys, unchanged Unicode string values and UTF-8
encoding. All schema object keys are ASCII, making the integer-only subset simple
to test without introducing a separate number-formatting implementation.

Sign these exact bytes (the prefix ends in one LF):

```text
UTF8("EpicTunerPluginManifest/v1\n") || canonical_manifest_bytes
```

`signature.json` has schema=1, algorithm="Ed25519", publisher key ID, SHA-256 of the
canonical manifest, and the detached 64-byte signature encoded as unpadded base64url
(86 characters). Reject noncanonical encoding. The signature binds the full manifest
and therefore every declared payload hash/length, target, capability and product.
The issuer's private key never enters the SDK, host package, logs or activation
responses. Test keys are separate from trusted publisher roots.

The native verifier uses OpenSSL Ed25519 and rechecks every payload hash before
each installed worker start. Import publisher public keys explicitly in the
Plugins window; its per-user `plugin-trust.json` has no built-in roots. A
`revoked: true` key blocks both new installs and installed version starts.
Rotate by importing a new key ID and signing an updated package; publisher ID
must remain stable across updates. Test keys in the private repository are
excluded from the SDK and normal app bundle. The runtime never treats a valid
test-vector signature as an implicit trust grant.

The publisher tool is `tools/package.py` in the exported SDK. It reads a full
v1 manifest, computes the inventory from a bounded payload tree, validates the
schema, signs the domain-separated manifest, and creates `.etplugin`. It
requires Python `cryptography` and `jsonschema` at publishing time; consumers
need neither Python nor those packages. Keep the private PEM outside the SDK,
package, source tree and support logs.

The manager stages a new version in a sibling directory, verifies it again,
renames it into an immutable version path, drains the prior worker, then
atomically saves the active-version registry. It preserves the prior version
and a separate settings snapshot for rollback. A failed new worker start
triggers rollback; manual rollback is also available. New capabilities appear
in the review dialog before the install action. Uninstall has an explicit
keep/remove settings choice. `--safe-mode` suppresses installed auto-start.

---

<a id="manual-chapter-15"></a>

Source: `docs/PREFABS.md`

## Host-rendered prefabs (SDK 1.0)

Discover `et.prefabs.v1` (UI capability) and `et.surface.v1` for the host Canvas
surface renderer. C uses the node types in `ui.h`; C++ authors use
`PanelBuilder::prefab(type, nodeId, targetId, parentId)`. Configure ordinary UI
properties; no Qt type, controller pointer or authored QML crosses the boundary.

| Node | Configuration / host behavior |
|---|---|
| FIELD_EDITOR | target is numeric field ID; resolved bounds, digits, units and enum options; Enter commits, Escape reverts; read-only and capability guards |
| CALIBRATION_TABLE | target is real table ID; existing heatmap, selection, keyboard/clipboard, axis operations, trace and undo/redo |
| CURVE_EDITOR | target is real curve ID; existing numeric grid, graph and grouped drag undo |
| TABLE_SURFACE | target is table ID; host isometric surface, rotation/zoom and data refresh |
| CHANNEL_PICKER | options are bounded newline metadata labels; value is selected label; TWO_WAY enables multiple selections; search and optional persistence |
| LIVE_PANEL | target is a channel ID; host snapshot quality/timestamp/units and bounded 512-point history; STEP configures 1..30 display Hz; empty target uses plugin DATA and VALUE |
| DATALOG_PANEL | host open-log picker, ranges, channel selection, cursors and graph; requires datalog.read; recording controls are hidden |
| ANALYSIS_PANEL | VALUE summary, DATA tabular preview, MINIMUM progress percent; emits analyze/cancel/apply COMMIT strings; TWO_WAY enables Apply only with host Apply permission |

Calibration targets are resolved by the host. Invalid IDs show an unavailable
state. Editors are per-instance and retain the established ET controls, colormaps,
clipboard parsing and keyboard behavior. Host capability/session/ownership guards
run before writes, including keyboard and clipboard actions. Undo/redo from a
prefab is rejected when an external edit has intervened. Prefab controllers are
not exposed to workers. All panel restoration rules in [UI.md](#manual-chapter-22) apply;
restoration never replays actions or calibration writes.

An empty calibration target selects an independent plugin dataset. DATA is
initial row-major TSV; COLUMNS is width, VALUE is the edited TSV. Set PERSIST to
retain VALUE with the panel. Dataset Enter/Escape, clipboard, undo/redo and finite
range validation remain local to the panel; COMMIT notifies the plugin. These
values never implicitly become a tune proposal. Graph datasets use sequential
point indices; host calibration IDs expose the complete INI axis semantics.

Prefab field/table/curve edits are explicit user edits through native controllers;
programmatic multi-field transactions use [TUNE.md](#manual-chapter-21). A worker cannot
obtain extra rights by enabling a button. Plugin panels use the current theme and
scale, and inherit workspace/floating/modal ownership and crash recovery.

---

<a id="manual-chapter-16"></a>

Source: `docs/PUBLISHING.md`

## Package and distribute a plugin

A plugin installation is a signed `.etplugin` archive containing a manifest,
target-specific libraries, declared dependencies, assets and license notices.
The host verifies the publisher and every inventoried file before loading code.
External redistribution terms for this SDK remain [pending](#manual-chapter-2).

### Development and distribution

Development mode accepts an explicitly named unsigned manifest for one launch.
It is useful for iteration. Installation uses the signed package manifest
described in [the package contract](#manual-chapter-14). Do not put a development
manifest directly into an archive and expect production installation to accept it.

### Prepare the payload

```text
payload-root/
  payload/windows-x64/my-plugin.dll
  assets/icon.png
  licenses/your-plugin.txt
```

Declare the matching target entry in your package manifest. Include only needed
files. Native dependencies belong under their target payload directory and must
be declared in `dependencies`. Images use package-relative `assets/...` paths.
Application source, captured ECU data, private keys and customer tunes never
belong in the payload.

Use the generated
[licensed example manifest](https://www.sgkembedded.com/epictuner/sdk/examples/licensed-feature-demo/package-manifest.json.in)
or [ImGui manifest](https://www.sgkembedded.com/epictuner/sdk/examples/imgui-inspector/package-manifest.json.in) as a
schema example. Replace the test publisher and placeholder product URLs, set
your host/ABI requirements and select the exact capabilities used by your binary.
The `files` inventory is populated by the packaging tool.

### Sign

```sh
python -m pip install -r tools/requirements.txt
python tools/package.py --manifest package-manifest.json --payload-root payload-root --key /outside/sdk/publisher.pem --output my-plugin.etplugin
```

Supply an external Ed25519 private PEM. The tool does not create or embed private
keys. It refuses an existing output path. Keep the resulting package immutable;
build a new semantic version for an update. Full canonicalization, inventory
and signature rules are in [PACKAGE.md](#manual-chapter-14).

### Verify and install

Prepare a publisher public-key file with `publisher`, `keyId`, `publicKey`
(32-byte hexadecimal Ed25519 public key). The imported file has exactly these three keys; the host adds revocation state to its trust registry. Review the
identity before importing it through **Tools → Plugins → Trust publisher key**.
Choose **Install package**, select the archive, review requested capabilities
and enable it. The host verifies native OS/architecture and required features.

For automation, the separate package checker takes the archive and a trust
registry containing `{"schema":1,"keys":[...]}`:

```sh
epictuner-plugin-package-check my-plugin.etplugin trust.json
```

That checker verifies the package; it does not install it or exercise the UI.
Run the actual application acceptance case after installation.

### Updates and recovery

An update stops the worker, stages and verifies an immutable version, then
switches the active registry. New capabilities require review. Failed updated
workers trigger rollback; plugin settings snapshots are handled separately.
Test schema migration and rollback before delivery.

Use `--safe-mode` if a plugin prevents normal startup. It suppresses third-party
starts while retaining management/recovery access. Do not repair a package by
editing its installed files; that invalidates its signature/inventory.

### Paid features and catalogs

Publisher trust authenticates the binary. Issuer trust authenticates an
entitlement. They are separate identities and checks. Follow
[licensing](#manual-chapter-12) for a local test issuer and offline activation.

The SDK's `tools/test-catalog.py` creates a signed local catalog for developer
acceptance. Its domain-separated signature authenticates canonical catalog content
and package hashes against an explicitly supplied public key. Production hosting,
catalog discovery, issuer operations, purchase/refund and publisher payouts
are outside this test catalog and remain later commercial-service work.

---

<a id="manual-chapter-17"></a>

Source: `docs/READ.md`

## Read services (SDK 1.0)

`epictuner/read.h` is a Qt-free C11 API. `epictuner/sdk/read.hpp` supplies C++20
wrappers and exception-contained callbacks. ABI 1.1 and all prior prefixes remain
unchanged. Query `ET_SERVICE_PROJECT`, `CHANNELS`, `DATALOG` or `SETTINGS` with
version `ET_SERVICE_VERSION_1` into an `et_read_service`. Each table permits only
its own operations. The host independently checks the manifest capability again.

| Feature | Manifest capability | Operations |
|---|---|---|
| `et.project.v1` | `project.read` | `PROJECT` |
| `et.channels.v1` | `channels.read` | `CHANNELS`, `SNAPSHOT`, `SUBSCRIBE`, `UNSUBSCRIBE`, `DERIVE`, `PUBLISH` |
| `et.datalog.v1` | `datalog.read` | `LOGS`, `LOG_CHANNELS`, `LOG_BATCH`, `LOG_SELECT` |
| `et.settings.v1` | `settings` | `SETTING_GET`, `SETTING_SET` |

### Ownership and asynchronous completion

Zero-initialize requests, set `struct_size`, operation and `count` (1..32).
`Reads::make` supplies these defaults. All calls require the worker dispatcher.
Inputs are copied before return. A successful `request` returns a nonzero operation
ID; it has exactly one terminal callback, including cancellation during shutdown.
An immediate error queues no callback. Keep callback context alive through that
callback. Reply arrays and strings are borrowed only during the callback; copy
anything needed by a background task. The C++ `ReadCallback` catches exceptions
inside the plugin's compiler/runtime and returns `ET_ERROR_CALLBACK`.

`cancel` races completion: an operation already completed keeps its actual result;
queued work completes with `ET_ERROR_CANCELLED`. There is no automatic retry.
Cancel cannot roll back a setting already persisted or a selected log range.
Read requests time out at 3000 ms; the worker fails instead of misattributing late
replies. Project replacement rejects queued old-generation operations. Stop/failure
revokes subscriptions and derived channels, discards queued work and cancels local
callbacks before destroying the plugin instance. Worker restart starts a new session.

`listen` installs **one read-event listener per worker**, shared by its read tables.
Subscribe from that listener or an asynchronous project reply, never by guessing a
generation. It receives the current project/connection on startup and coalesced
changes, including close/reopen at the same path. The project reply contains the
display name in `text`, `total` 0/1 for closed/open, `project_generation`, and
`ET_READ_CONNECTED` in `flags`. Private paths and tune bytes are not exposed.

### Operation fields

All project-scoped requests must echo the current `project_generation`. User
settings and the project query are global. Unused request fields must be zero or
empty (the host currently tolerates unused valid fields for forward compatibility).
Unsupported flags and non-finite numbers are rejected. The `id`/`text` UTF-8 spans
are limited to 4096 bytes; channel IDs are at most 256 bytes, 16 distinct selections.

- `CHANNELS`: metadata page at `offset`, up to `count` records. `total` reports the
  catalog size; each item provides ID, display label, units and derived flag.
- `SNAPSHOT`: `channels` selects values. Items contain finite `value`, source
  `timestamp` (Unix seconds for live data), `sequence`, units and flags. Invalid
  numeric values use a zero payload **without** `ET_READ_VALID`; never interpret
  that payload as a valid measurement. Disconnection or no fresh source for 500 ms
  sets `ET_READ_STALE`. Freshness uses a host monotonic clock.
- `SUBSCRIBE`: `channels` and flags 0 (analysis batches) or `ET_READ_LATEST`
  (coalesced display). Completion returns a session/project-bound subscription
  handle. `UNSUBSCRIBE` consumes that handle. Close panel-owned subscriptions
  explicitly; the example does so on its close event. Background subscriptions
  may remain active until explicit unsubscribe, project close or worker stop.
- `DERIVE`: `id` is `<plugin-id>:<local-name>`, `text` is units, `channels` are
  immutable dependencies. At most 16 derived channels per worker. Derived
  metadata is visible to read-service clients; its label lists dependency IDs.
  Registration rejects unknown dependencies, duplicates and dependency cycles,
  including cycles across a publisher restart. `PUBLISH` supplies ID, value,
  timestamp and `ET_READ_VALID` when valid. Publication must match the latest
  source sample; stale calculations are rejected. The host also checks dependency
  validity. Values become invalid when the source advances or a dependency leaves.
  Plugins cannot overwrite ECU channels or another publisher's values. This does
  not inject channels into the ECU definition, recorder, or tune expression engine.
- `LOGS`: pages host-approved **already-open** log IDs; item label is the basename
  and value is the current row count. No plugin-provided path is opened.
  IDs are revoked when the log closes or is replaced. `LOG_CHANNELS` pages a log's
  columns using log ID in `id`; column IDs are opaque strings to echo in requests.
- `LOG_BATCH`: log ID, column IDs in `channels`, row `offset` and `count`. Returns
  row-major values with log-relative timestamps, row index in `sequence`, validity
  and units. `total` is current rows. The returned page can be shorter than requested
  to respect the 48 KB payload budget; advance by actual returned rows. A log can
  grow during recording; request a fixed ending row for reproducible analysis.
- `LOG_SELECT`: log ID plus row offset/count selects that already-open log and
  its host view range. Busy or unavailable logs are rejected. Export and recording
  control are outside these read tables; the user uses EpicTuner's existing controls.
- `SETTING_GET`: `id` is a simple key, `schema` is the expected nonzero schema;
  flags 0 select per-user storage or `ET_READ_PROJECT_SETTING` select per-project
  storage. A mismatch returns `ET_ERROR_CONFLICT` **with the old schema and text**
  so the plugin can migrate it. Missing keys return `ET_ERROR_NOT_FOUND`.
- `SETTING_SET`: `text` is the replacement, `schema` its new schema, `offset` is
  the expected previous schema (0 means create only). This explicit comparison
  prevents accidental schema downgrade/overwrite. Storage is namespaced by
  authenticated plugin ID and hashed project identity, at most 64 keys per scope.
  It persists across worker/app restart. Storage is plain text: do not store
  passwords, tokens, licenses or other sensitive material. There is no secrets
  storage claim. Settings do not grant ECU write access.

### Bounded transport and loss reporting

There are 32 outstanding read operations and 100 requests/second per worker,
within the shared 200-message transport ceiling. The public limit of 64
subscriptions applies per worker. Source ingestion has a 128-snapshot mailbox;
the broker retains 128 snapshots. Neither source path queues a GUI event per
sample or calls plugin code. Existing comms/recording remain on their own thread.

At most one read event per worker is unacknowledged. The proxy acknowledges after
the listener returns; a slow listener therefore cannot fill the socket with data.
The broker distributes at most 50 events/second per worker, round-robin across
subscriptions, with at most 32 rows/16 channels/512 items and 48 KB per event.
`gaps` counts skipped **source snapshots** since the preceding event, including
mailbox overflow, analysis overflow and intentional latest-only coalescing.
Sample sequence numbers preserve the underlying source identity. A fresh
subscription starts after the current sample. Silence crossing the stale threshold
produces one stale notification with the last sample sequence; connection events
also invalidate displayed values. Old subscriptions never resume after project
replacement. There is no lossless-recording promise for this bounded stream.

### Examples and validation

`live-telemetry` uses 16-channel catalog pages, a picker, numeric readout and graph,
timestamps/units/gaps, connection events and a compiled RPM/2 derived channel.
`log-analyzer` selects open logs/channel/row range, processes bounded batches in
cancellable background jobs, and compares two logs using result table/chart controls.
The example picker shows the first 32 open logs and first 32 channels; the API
supports paging beyond those example limits. M4 supplies the dedicated calibration
and datalog prefab library; these examples use M2's general ET controls.

Build either example with `find_package(EpicTunerSDK 0.5 CONFIG REQUIRED)` from
the SDK export. The public headers and example sources do not link Qt or private
application code. Each directory contains its CMake project and development
manifest template. `live-telemetry/synthetic.ini` is a safe mock definition.

Host verification: `plugin_reads` covers validation, handles, generations, derived
validity, settings schemas, cancellation and flood admission. `plugin_reads_app`
uses the actual app, mock ECU and two generated MLG files, validates exact results,
disconnect/reconnect, panel release, cancellation and stale log/project rejection.
`plugin_reads_load` compares a 30-second baseline against a 30-second five-worker
fault run while 100 Hz mock polling and recording continue. Evidence records poll
interval percentiles, missed intervals, recorded rows and explicit stream gaps.
An interval of at least 20.5 ms at 100 Hz counts as one missed poll interval
(two periods, with 0.5 ms allowance for the millisecond timestamp resolution).
These are software gates, not physical ECU/Pi or endurance certification.

---

<a id="manual-chapter-18"></a>

Source: `docs/SDK-PRE2.md`

## SDK 1.0.0-pre.2 migration

Use EpicTuner 0.8.3 or later. This is a source-helper and host-diagnostics update;
C ABI 1.1, service table layouts and existing numeric constants are unchanged.

### Dropdown selection: properties and events are different domains

`ET_UI_SELECTED` was always an **event type**, not a property. It has the same
numeric value as the unrelated `ET_UI_UNITS` property. Renumbering it would break
existing event handlers. Prefer the explicit names added in this SDK:

- Set a dropdown's index with `ET_UI_SELECTED_INDEX` (alias of `ET_UI_VALUE`).
- Receive selection events as `ET_UI_EVENT_SELECTED` (legacy `ET_UI_SELECTED`).
- Both the selected property and event value use `ET_VALUE_F64`: a zero-based
  index, not the option text. `-1` is the no-selection property value.

```cpp
panel.add(ET_UI_SELECT, "model").text(ET_UI_OPTIONS, "").selectedIndex(-1);
// After a Make selection, update options and index together in one atomic patch:
ui.patch(e.panel, {Ui::text("model", ET_UI_OPTIONS, "Model A\nModel B"),
                  Ui::selectedIndex("model", 0)}, e.revision, e.origin, &completion);
```

Do not place `ET_UI_SELECTED` in a property array or a patch. The host now reports
the node, offending property and correction instead of a generic type error.
Empty options render an empty, disabled control. Populate options and choose an
index in the same patch. When replacing a short list, reset its old index too.
For a single option, set index `0` to autoselect it. Programmatic selection does
not generate a user selection event: update dependent controls and the preview
in the same plugin logic instead of waiting for a callback.

### Several patches from one event

Before host 0.8.3, the first successful patch advanced the panel revision and
subsequent calls using that event's revision failed with `ET_ERROR_CONFLICT`.
This affected every property, including a status update followed by options.

Host 0.8.3 queues synchronous patch calls made inside the same UI event callback,
for the event's panel, revision and origin. It applies them in call order, using
each successful completion's revision for the next call. A failed call cancels
its remaining dependent calls. Intervening user edits still cause a conflict;
they are never silently overwritten. Every call keeps its own completion.
Host **0.8.4** extends this to calls using the SDK's default origin `0`, including
a status/render call before the options call. Use 0.8.4 or later for that pattern.

Prefer one atomic patch for mutually dependent options, indexes and previews.
Separate status/render patches may precede it. Calls made later from timers or
completion callbacks must use a fresh revision (the successful patch completion
returns it as `ET_VALUE_U64`). Do not reuse a saved event revision indefinitely.

### Patch errors and startup failures

Patches remain atomic: partial application would leave dependent controls
inconsistent. `ET_OK` means queued. A completion supplies the actual result and
`detail`; copy or log that borrowed string inside the callback. Rejections are
also visible in Tools > Plugins diagnostics without a completion callback.

The host reports named service failures and immediate UI validation failures when
startup fails. For plugin-specific operations, use
`return context.check(result, "Load base tune database")` to attach the step name.
An asynchronous definition failure must be handled in its completion; returning
from `start()` cannot synchronously report a later host reply. Keep the callback
owner alive and return `ET_OK` when the definition has been queued successfully.

### Empty table previews

Plugin-owned calibration tables use tab-separated cells and newline-separated
rows, with `ET_UI_COLUMNS` matching the row width (12 for a 2-row, 12-column table).
Set a range covering every finite value. `ET_UI_VALUE` contains the edited/current
dataset and takes precedence over initial `ET_UI_DATA`. For refreshed previews,
patch `ET_UI_VALUE`; otherwise a previous edited value can hide new initial data.
Keep `ET_UI_TARGET` empty for plugin-owned data. A rejected atomic patch changes
neither the dropdowns nor the preview. Inspect the completion before assuming the
renderer failed. The client now displays an explicit message for empty previews.

See [the control/event tables](#manual-chapter-22), [the first-plugin tutorial](#manual-chapter-10),
and the [cascading-select example](#manual-chapter-27) for a complete Make → Model → Tune flow.

---

<a id="manual-chapter-19"></a>

Source: `docs/SDK-PRE3.md`

## SDK 1.0.0-pre.3: actionable diagnostics

Recommended host: **EpicTuner 0.8.5 or later**. C ABI 1.1 and all service layouts
are unchanged. Existing compiled plugins receive the richer host diagnostics
without recompiling. Recompile to use the new C++ source helpers.

### Read the actual failure

Tools > Plugins now shows UTC timestamps and INFO/WARN/ERROR labels. Use
**Copy diagnostics** to copy the plugin identity, version, error and retained log.
The log is bounded; capture a failure promptly instead of flooding retries.

Host UI rejections include the symbolic result, operation, request ID, panel and
instance key, and up to three affected node/property targets. Conflict messages
include the submitted and current revision and whether the latest change came
from an accepted plugin patch or a user event. Detail is bounded to 512 characters;
long identifiers or many targets can be truncated. Patch values are not logged.

Example (illustrative IDs and revisions):

```text
request=42 ui.patch ET_ERROR_CONFLICT(12) panel='org.example.loader.main/': submitted=6 current=7; last change: accepted plugin patch. Use completion.value.u64 after success or the latest event.revision; rebuild after user input, do not retry a stale patch.; targets=[model.options; model.value]
```

If the last change was **accepted plugin patch**, a previous successful update
advanced the revision. A timer or completion callback must stop reusing the old
event revision. If the last change was **user event**, recompute from that new
event; retrying an old model can overwrite a user's newer choice. The last-change
description reports the most recent mutation, not a complete revision history.

### Log both immediate and asynchronous results

`ET_OK` returned by `ui.patch` means queued, not applied. An immediate failure
does not schedule a completion. Check both paths. Store `Context` by value (as
the starter does); the `start()` reference itself is temporary.

```cpp
static void ET_CALL completed(void* owner, const et_completion* c) noexcept {
    auto& self = *static_cast<MyPlugin*>(owner);
    try {
        self.context->check(*c, "Populate Model/Tune and preview");
        if (c->code == ET_OK && c->value.kind == ET_VALUE_U64) {
            // This callback belongs to one known panel. Never move backwards
            // if a newer event for that same panel has already arrived.
            self.revision = std::max(self.revision, uint32_t(c->value.data.u64));
        }
    } catch (...) { /* Never let exceptions cross the ABI. */ }
}
// On the dispatcher, with a persistent completion owner:
context->check(ui.patch(panel, changes, revision, 0, &completion), "Queue preview");
```

`Context::check(const et_completion&, step)` logs failed completions, including
request/operation ID, symbolic/numeric result, integer revision when present, and
the borrowed detail copied inside the callback. `completionMessage(c, step)`
returns the same string for your own logger. `resultName(code)` returns a stable
symbolic label, including `ET_ERROR_UNKNOWN` for unknown codes. These are header
helpers, not new ABI entry points. Declare the `log` capability for SDK logging.

For UI patch completions, `ET_VALUE_U64` is the panel revision, including on a
conflict. Do not treat integer values from other services as UI revisions.
Use separate state and completion context for each panel; reset it on close or
reopen. A failed patch does not authorize replay against its reported revision.

### Avoid repeated conflicts

- Prefer one atomic patch for related options, selected indexes and preview data.
- Synchronous patches inside one event are sequenced by host 0.8.4+ (including
  default origin 0). This does not extend to later timers or job completions.
- For deferred updates, allow one in-flight patch per panel, retain the latest
  event and completion revisions, and coalesce newer display state while waiting.
- On conflict, report it once and wait for/recompute from fresh state. Do not
  increment a guessed revision or repeatedly resubmit the same patch.
- A failed event-chain predecessor cancels its dependent patches. The cancellation
  names the predecessor request and preserves its error context.
- Immediate limit diagnostics report pending/rate budgets; reduce update volume.
  A timeout reports the request, operation and timeout before stopping the worker.

The starter and cascading-select examples demonstrate completion logging. See
[UI contracts](#manual-chapter-22) and [troubleshooting](#manual-chapter-20) for lifecycle,
dispatcher, ownership and validation rules.

---

<a id="manual-chapter-20"></a>

Source: `docs/TROUBLESHOOTING.md`

## Troubleshooting

Start with the Plugins manager's state, error and per-plugin log. Preserve the
plugin version, host version, target architecture and exact failing action.
Use synthetic fixtures to produce a shareable reproduction.

Host 0.8.5 adds **Copy diagnostics**, timestamps, named UI errors, request/panel
context, affected properties and revision-conflict explanations. SDK pre.3 adds
completion logging helpers. See [actionable diagnostics](#manual-chapter-19) for code
and for resolving stale timer/completion revisions without overwriting user input.

### Build and load problems

| Symptom | Check |
|---|---|
| CMake cannot find EpicTunerSDK | Point `CMAKE_PREFIX_PATH` at the extracted SDK root, not its include directory |
| CMake rejects the SDK version | Change a preview `0.x` package request to `1`; use a new build directory |
| Compiler lacks C++20 | Use a supported native compiler and `EpicTuner::SDKCpp` |
| Query symbol missing | Use `ET_PLUGIN` or the C export/calling convention; inspect `et_plugin_query` |
| Library fails to load | Match OS/architecture and inspect missing native runtime dependencies |
| Development plugin is absent | Supply both `--plugin-development` and the explicit absolute manifest path |
| All plugins stay stopped | Check `--safe-mode` and the plugin's enablement/license state |
| Wrong identity or missing grant | Match manifest ID/capabilities with the binary declaration |
| Image unavailable | Use an inventoried package-relative asset and copy it into development layout |

### Asynchronous API errors

An immediate error means no completion was queued. An immediate `ET_OK` means
the operation was queued; inspect its terminal completion. Keep callback owners
alive and never retain borrowed reply strings/items.

For `ET_ERROR_STATE`, check the callback thread and plugin/project lifecycle.
Host services are dispatcher-only. A background job may compute and observe its
cancellation token; it cannot call UI/read/log services.

For `ET_ERROR_LIMIT`, reduce batch size, page metadata, coalesce display updates,
and wait for outstanding completions. Do not spin or flood retries.

For stale handles or conflicts, discard the old preview/subscription and obtain
new context. Read project generation, tune definition, revision and stored values
again. Recompute the proposal. Never retry a timed-out Apply automatically.

### UI problems

Use stable namespaced panel IDs and unique node IDs. Each node's parent must
exist and the tree must be bounded and acyclic. Match property types from
[UI.md](#manual-chapter-22). On patch rejection, keep the last valid model and report the
error; do not manufacture a new revision by incrementing a local guess.

If a panel becomes disabled, inspect worker failure first. A blocking callback
can trigger heartbeat termination. Close and restart through the host; restored
panel visibility does not replay button actions.

### Live data and logs

Disconnected, stale or invalid data is not zero. Test `ET_READ_VALID` and stale
flags before using a number. Expose sequence gaps to the user. Display
subscriptions intentionally coalesce updates; analysis subscriptions still have
finite buffers and must handle overflow.

Open logs through EpicTuner before enumerating them. Log IDs and selected channel
IDs belong to the current host context. A replaced/closed log cancels reads.
The analyzer fixtures provide exact expected means for a known-good comparison.

### Signing and activation

A publisher key and an issuer key have different roles. Trust each explicitly
for its purpose. Hash mismatch means a payload changed after signing; rebuild
and sign a new package rather than disabling verification.

An offline response must match the current installation, challenge nonce,
product and version rights. Export a fresh challenge after reinstallation or
another activation request. For subscriptions, inspect expiry and lease/grace
status. The test issuer accepts loopback HTTP; production activation requires
HTTPS.

### Build a useful bug report

Include the compiler/target, manifest, minimal source, synthetic fixture and
exact steps, together with relevant plugin diagnostics. Remove activation
secrets, private tune/INI data and captured customer logs. State whether the issue
reproduces in the checker, the development host, or the installed signed package.

---

<a id="manual-chapter-21"></a>

Source: `docs/TUNE.md`

## Calibration service (SDK 1.0, et.tune.v1)

The optional `ET_SERVICE_TUNE` table is an `et_tune_service`, layout-compatible
with `et_read_service`. Negotiate version 1.0. `tune.read` is needed to negotiate;
every operation is independently checked against the worker session's granted
capabilities at both ends. Existing C ABI 1.0/1.1 prefixes are unchanged.
Use `epictuner/tune.h` from C, or `epictuner/sdk/tune.hpp` from C++20.

All requests and callbacks run on the worker dispatcher. Inputs are copied before
return, replies are borrowed until callback return. Limits, cancellation,
acknowledgement and terminal-callback semantics are those in [READ.md](#manual-chapter-17).
Cancelling before dispatch prevents local changes. An already accepted commit
is authoritative; cancellation is never a rollback. A transport timeout never
means it is safe to retry a write: refresh first.

### Request mapping

Initialize with `Reads::make(operation, projectGeneration)`. Unused fields must
remain at their defaults. `count` is 1..32 for catalog/snapshot pages. A revision
is a monotonically advancing 32-bit host token, returned in reply `schema`.
Project generations are the same as the project/read service. Revision changes
are conservative: manual edits, AutoTune, project/save and connection changes
invalidate an old preview. Never derive a revision by adding to it.

| Operation | Request | Reply |
|---|---|---|
| CATALOG | offset/count | Paginated field/table/curve IDs; item label is kind, units is field units or backing value field; text is definition SHA-256; schema is revision |
| SNAPSHOT | id is field/table/curve ID; offset/count are value indices | items contain actual stored physical values and element indices in sequence; text is bounded JSON metadata |
| PROPOSE | id is definition SHA-256; schema is base revision; text is proposal below | Host-owned proposal handle and normalized preview values; does not modify calibration |
| APPLY | same definition/base revision plus proposal handle | Consumes that session-owned proposal; actual stored values and local acceptance flags |
| UNDO / REDO | definition and current revision | One labeled coherent group for the last accepted plugin transaction; rejects if another edit intervened |
| STATUS | generation | Current RAM readback status in flags/text; revision in schema |
| BURN | definition and current revision | Opens host confirmation; does not burn on receipt; requires separate tune.burn capability |
| DISCARD | proposal handle | Releases an unused preview; no apply capability needed |

PROPOSE and DISCARD require `tune.propose`; APPLY/UNDO/REDO require `tune.apply`.
CATALOG/SNAPSHOT/STATUS require `tune.read`. Handles are scoped to worker session
and project generation. There are at most eight outstanding proposals per worker;
DISCARD unused previews. Refreshing a revision retires older previews. Restart,
project replacement and disconnect invalidate proposals without replaying them.

A proposal is UTF-8 TSV, with a nonempty label of at most 128 characters on its
first line, followed by 1..64 lines `field<TAB>index<TAB>expected<TAB>requested`.
The entire payload is at most 4096 bytes. Field names and labels cannot contain
embedded tab/newline/NUL. Use the supplied encoder rather than locale-dependent
number formatting:

```cpp
const auto body = et::sdk::Tune::proposal("Fuel correction", {
    {"fuel", 0, oldValue, newValue}
});
auto request = et::sdk::Reads::make(ET_TUNE_PROPOSE, generation);
request.id = et::sdk::string(definitionHash);
request.schema = baseRevision;
request.text = et::sdk::string(body);
tune.request(request, previewCallback);
```

Preview/result items use `id` = field, `sequence` = element index, `timestamp` =
expected old value, `value` = normalized stored value. This mapping is specific
to tune operations; `timestamp` is not a time for a tune item.

Snapshot JSON includes id, units, digits, bounds and whether each bound exists,
shape, enum value/label pairs, readOnly, and native field type. Table/curve metadata
also includes kind, title, axisLabels, backing x/y/z field IDs and read-only axes.
Read their backing fields separately to obtain axis values. String/PC-variable
and protected/read-only axis writes are rejected; this numeric service never
exposes raw page pointers. Unsupported metadata exceeding 4096 bytes returns a
limit error rather than truncating a definition.

### Correctness and ECU outcomes

The host validates every element on an isolated copy using ProjectManager's
storage/scaling rules. It rejects malformed numbers, bounds, dimensions, duplicate
cells, old-value conflicts, protected axes and overlapping/scaling changes whose
final stored values disagree with the proposed normalization. It serializes and
reads back the candidate before publishing the live state once. The normal host
write queue receives changed spans after local acceptance. There are no partial
local commits on validation failure; undo reverses the complete accepted group.

`LOCAL_ACCEPTED` is distinct from `RAM_UNVERIFIED`, `RAM_VERIFIED`, `RAM_FAILED`,
`OFFLINE`, and `FLASH_PENDING`. In connected mode the comms thread drains the
normal write queue and reads ECU pages to verify the exact accepted snapshot.
STATUS reports the result only while the project/revision still matches. A failed
or mismatched readback retains the local tune and directs the user to the normal
synchronization flow. It does not replay or silently roll back physical writes.
Whole-page readback briefly pauses polling on the communications thread.

Firmware updates, tune synchronization and packet-proxy ownership block edits.
Disconnected operation is rejected unless the project is explicitly offline.
Only verified Serial/TcpBridge/Mock local project connections are supported;
remote sessions are rejected. Burn is a separate confirmation tied to the current
session, generation and revision. The existing Burn flow reports its result;
opening a panel, reconnecting, proposing, applying or restoring state never burns.

See [PREFABS.md](#manual-chapter-15) and the two calibration examples. The fuel demo
accepts offline/Mock proposals only and has no Burn capability.

---

<a id="manual-chapter-22"></a>

Source: `docs/UI.md`

## 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](https://www.sgkembedded.com/epictuner/sdk/examples/hello-panels/hello.cpp)
is compilable author code, with no private host includes or QML.

### Build, install for development, and open

```sh
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](https://www.sgkembedded.com/epictuner/sdk/docs/SDK-PRE2.html#several-patches-from-one-event).
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](#manual-chapter-18) 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](#manual-chapter-15); 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.

```cpp
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](#manual-chapter-26).

Alternatively, one definition can allow both placements and multiple instances:

```cpp
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.

---

<a id="manual-chapter-23"></a>

Source: `docs/VERIFICATION.md`

## Building and checking the SDK contract

Extract the SDK archive into a fresh directory. Neither the application source nor
Qt is required. Use CMake 3.24+, a 64-bit C11/C++20 compiler and its normal native
runtime. Compile separately for Windows x64, Linux x64 and Linux ARM64. macOS is a
future target. The SDK contains no bundled compiler, Qt, crypto library or binaries.

```sh
cmake -S /absolute/sdk/tests -B consumer-build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build consumer-build --config Release
ctest --test-dir consumer-build -C Release --output-on-failure
cmake -S /absolute/sdk/tests/c-only -B c-only-build -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build c-only-build --config Release
ctest --test-dir c-only-build -C Release --output-on-failure
```

On Windows, use forward-slash absolute paths and an x64 MSVC developer shell or
MinGW x64 toolchain. For Ninja, explicitly select `-DCMAKE_C_COMPILER=cl` and
`-DCMAKE_CXX_COMPILER=cl` for MSVC, or `gcc`/`g++` for MinGW. Keep each compiler's
build directory separate. Multi-configuration generators use `--config Release`.

The four conformance cases exercise C layout, C++ ownership/exception/value/version
contracts, and actual C/C++ shared-module query/start/event/stop calls. Binary
fixtures negotiate both ABI 1.0 and 1.1 and reject incompatible hosts. Assertions
remain active in Release. The fifth case configures with only a C compiler enabled,
checking that the C imported target never requires the C++ toolchain.

For ARM64 cross-compilation, set a CMake toolchain with an AArch64 compiler/sysroot
and `CMAKE_CROSSCOMPILING_EMULATOR` pointing to QEMU plus its `-L` runtime root. The
same shared-module tests then execute as ARM64 code. Emulation does not establish
physical Raspberry Pi, worker supervision, GUI, renderer or hardware acceptance.

The private repository additionally tests the worker/broker, schema/signature
vectors, stale handles, cancellation/commit rules and a five-worker transport
benchmark. Those host implementation tests and test signing keys are intentionally
excluded from this author export. Signed package verification and the v1 application
services are implemented. Production commercial operations remain tracked in DECISIONS.md.

The independent ImGui example needs native OpenGL
and GLFW platform development headers, which the core ABI suite does not. On
Windows and Linux, use the worker checker's `--custom-window` mode described in
the ImGui example README. The host repository also has native application/input
acceptance scripts; those are host-maintainer tests and are not SDK dependencies.

Install `tools/requirements.txt`, then run `python tests/test_tools.py` for
catalog authentication/inventory, starter generation and website link regressions.
Use `tools/build-docs.py --output site` to build and validate every website page.

`SHA256SUMS.txt` inventories every exported file other than itself. Verify those
hashes after extraction. This is a prepared SDK v1 release candidate. The product
owner requested preparation with redistribution terms pending; see REDISTRIBUTION.md.

---

<a id="manual-chapter-24"></a>

Source: `docs/WEBSITE.md`

## Website build and SGK integration

The SDK includes a static developer site and a proposed replacement EpicTuner
landing page. They share SGK Embedded's dark surface and red accent. The product
page links to practical onboarding and the SDK reference.

### Generate and preview

```sh
python -m pip install -r tools/requirements.txt
python tools/build-docs.py --output site
python -m http.server 8080 --bind 127.0.0.1 --directory site
```

Open `http://127.0.0.1:8080/epictuner/` and
`http://127.0.0.1:8080/epictuner/sdk/`. Search works locally without a backend
or network request. Code blocks have copy buttons; articles include page anchors,
source links, keyboard focus styling and responsive navigation.
Every page also links to `EpicTuner-SDK-Manual.md`: one generated UTF-8 file
containing all guide chapters, public C/C++ headers, schemas and complete starter
and cascading example sources. Authors can download it and give it to their
coding agent. A `.md.sha256` sidecar identifies the exact generated manual.

The generator verifies local links, anchors and images and fails on a broken
link or a reference outside the public SDK. To recheck existing output:

```sh
python tools/build-docs.py --output site --check
```

### Deployable structure

```text
site/epictuner/index.html          Product landing page
site/epictuner/downloads/index.html Packages, requirements and release status
site/epictuner/assets/             Shared styling, scripts and screenshots
site/epictuner/sdk/index.html      SDK home
site/epictuner/sdk/docs/           Guides and API contracts
site/epictuner/sdk/examples/       Example guides, source and synthetic fixtures
site/epictuner/sdk/include/        Public C/C++ headers
```

The generated files can be served from a static route under
`www.sgkembedded.com/epictuner/`. All internal links are relative, so a staging
subdirectory works too. If the existing site framework owns `/epictuner`, port
`website/landing.html` into that route and serve the SDK output under
`/epictuner/sdk/`. Preserve path casing or redirect old links consistently.

No changes to the live SGK website are made by this generator. The landing page
links to a downloads page with package descriptions, provisional requirements
and disabled Coming soon buttons. Add artifact URLs, versions, sizes, SHA-256
checksums and release notes when an approved release is available. The proprietary SDK terms permit plugin development and distribution, but prohibit
redistributing the SDK itself; see `../REDISTRIBUTION.md`.

### Edit the content

Edit Markdown under `docs` and example READMEs. The generator uses those same
sources in the SDK archive and the website. Edit `website/page.html` for the
documentation shell, `website/landing.html` for product copy,
`website/downloads.html` for packages and requirements, and
`website/assets/site.css` for the shared design.

Product screenshots are real SDK example captures using synthetic data. Replace
them with fresh approved captures when the UI changes. The page avoids the old
landing page's public-GitHub claim: repository publication and production release
hosting are separate decisions.

---

<a id="manual-chapter-25"></a>

Source: `docs/WINDOW.md`

## Independent custom windows (M6)

`et.custom-window.v1` is an optional C service. Declare the `custom-window`
capability and feature in the plugin manifest, then request
`ET_SERVICE_CUSTOM_WINDOW` or use `et::sdk::Window`. The service registers one
worker-owned window per plugin. `show()` opens or focuses it; `focus()` and
`close()` require no broker UI model. A user close is reported through `is_open`
after the next `tick()`. The worker calls `tick()` at approximately 60 Hz.

All calls and callbacks are serialized on the worker dispatcher. Native window,
graphics and ImGui pointers stay in the plugin module; no pointer crosses IPC.
Callbacks should return promptly so heartbeats and ET service results can run.
Exceptions or failed frames fail only that plugin worker. Worker shutdown closes
the window before the plugin's `stop()` callback; `stop()` should also clean up
idempotently. Background jobs cannot invoke the window service.

The [imgui-inspector](#manual-chapter-31) example builds
against the exported SDK. It uses GLFW 3.4 and Dear ImGui 1.92.3 with OpenGL 3.3
on Windows and Linux X11. Both source distributions and their licenses are
included under `vendor/`. On Linux, install OpenGL and X11 development packages
including Xrandr, Xinerama, Xcursor and Xi. The example has no Qt dependency.

Open **Examples / ImGui inspector** in the Plugins menu, then use **Open / reopen**.
The ET panel offers focus and close; the independent window plots live RPM and
can select another channel or refresh ET metadata. The native window uses GLFW
input and updates ImGui font scale from the current monitor content scale.
This is an independent OS window, not an embedded or docked Qt Quick surface.

Windows acceptance: `scripts/verify-plugin-window.ps1` builds the exported
example, verifies actual nonuniform OpenGL framebuffer pixels, native mouse and
keyboard events, resize, close/reopen, worker crash containment and ET panel
registration. It can run with a system-only PATH against a staged host. Linux
uses the same source/backend with X11. Pi/ARM64 rendering, a physical second
monitor, monitor hot-plug and Wayland are separate platform gates.

---

<a id="manual-chapter-26"></a>

Source: `examples/calibration-workbench/README.md`

## calibration-workbench

![Docked calibration workspace using synthetic data](https://www.sgkembedded.com/epictuner/sdk/examples/calibration-workbench/screenshot.png)

SDK-only C++20 example, with no Qt or private application headers. The shared
implementation is `../common/calibration.hpp`. Build from the exported SDK:

```sh
cmake --preset release
cmake --build --preset release
```

Alternatively pass `-DCMAKE_PREFIX_PATH=/absolute/path/to/sdk` to a normal CMake
configure. Start the host with `--plugin-development --plugin-manifest` followed
by the absolute generated `build/plugin.json` path. The Plugins menu opens the
panel. Safe mode overrides development opt-in. For a local package, place the
built library and generated manifest together; preserve the manifest entry name.
For signed installation, use the [test catalog](#manual-chapter-8) or
the [publishing guide](#manual-chapter-16).

Create an isolated project from `synthetic.ini`; select Mock or work offline. The workbench tabs
show the field/table/curve/surface, channels, datalog and plugin dataset prefabs.
Refresh snapshots the current revision. Set the proposed setting, click Preview, inspect normalized values, then Apply preview. Undo/Redo acts on that complete accepted group. Verify RAM reports the separate ECU outcome. Request Burn opens a separate host confirmation.

Acceptance: the fixture is seeded to 100 percent fuel by the application test.
Integer request 12.7 previews/stores 13; fuel request 101.26 previews/stores 101.3. Intervening manual/AutoTune changes reject the stale proposal. Clipboard/keyboard and save/reopen checks use the existing native editors.

### Docked and popout together

![ET-styled calibration popout with the same host prefabs](https://www.sgkembedded.com/epictuner/sdk/examples/calibration-workbench/popout.png)

The main panel explicitly permits `ET_PLACEMENT_WORKSPACE`. Click **Open popout**
to open a second complete workbench in an ET-managed floating window, explicitly
declared with `ET_PLACEMENT_FLOATING`. **Show docked** focuses or reopens the main
workspace. Both can remain visible at the same time. The Plugins menu also lists
the popout directly. Each definition is a singleton: repeated opens focus it.

The shared implementation registers `.main` and `.popout` and routes UI events by
`event.definition`. Each view owns its callback state, proposal, UI revision and
preview inputs. Host calibration prefabs read/write the same project; a change
from either view updates the other. A proposal prepared before another edit must
be refreshed and recomputed. Closing one view does not close the other.

For multiple instances of one definition instead, use `ET_PANEL_MULTIPLE` and
nonempty instance IDs. Keep callback/model state separate for each instance.
See [panel placement examples](https://www.sgkembedded.com/epictuner/sdk/docs/UI.html#explicit-docked-and-popout-panels).

Acceptance: open both views, edit a field/table cell and observe both; change
Proposed setting in one view and check the other input is unchanged. Close and
reopen the popout; the docked workspace stays usable. Tab state persists per view.
These synthetic fixtures do not establish physical ECU certification.

---

<a id="manual-chapter-27"></a>

Source: `examples/cascading-selects/README.md`

## Cascading dropdowns and a table preview

This SDK-only example reproduces a Make → Model → Tune workflow using synthetic
data. Selecting a make populates models, selecting a model populates tunes, and
selecting a tune displays a 2 × 12 preview table. Changing an upstream selection
clears dependent selections and the preview in one atomic patch. It never accesses
an ECU or copies a customer's plugin/database.

Requires host 0.8.3 or later. Build with CMake/Ninja like the
[starter tutorial](#manual-chapter-10), substituting this example's
directory. Load its generated `plugin.json` and open Examples/Cascading selections.
The module is `cascading-selects.dll` or `cascading-selects.so`.

See [plugin.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/cascading-selects/plugin.cpp) for explicit `ET_UI_EVENT_SELECTED` / F64 events,
`Ui::selectedIndex`, empty options, completion diagnostics, and refreshing a
plugin-owned table through `ET_UI_VALUE` rather than stale initial `ET_UI_DATA`.
## Successive patches and autoselect

Each selection deliberately sends a status patch before the dropdown/preview
patch, using the same event revision. Host 0.8.3 applies these in order. Selecting
Yamaha demonstrates automatic selection of a single model and tune, including
an immediate preview without requiring extra clicks. Programmatic selection does
not emit user-input events.

---

<a id="manual-chapter-28"></a>

Source: `examples/fault-injection/README.md`

## Fault injection — test only

![Fault controls in the native application with a synthetic project](https://www.sgkembedded.com/epictuner/sdk/examples/fault-injection/screenshot.png)

This deliberately faulty plugin is supplied for recovery testing in a disposable
project. No fault executes on startup. It is excluded from ordinary customer
autoload and marked test-only in the local signed catalog.

### Build and run

Set `EPICTUNER_SDK_ROOT`, then run `cmake --preset sdk` and
`cmake --build --preset sdk`. Launch the development manifest explicitly:

```sh
epictuner --plugin-development --plugin-manifest /absolute/path/out/plugin.json
```

Use `epictuner.exe` on Windows. Open **Plugins → Tests → Fault injection**.
For signed installation use the [test catalog](#manual-chapter-8).
The synthetic fault commands are implemented in [fault.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/fault-injection/fault.cpp).

### Acceptance cases

| Command | Expected result |
|---|---|
| crash | Worker exits with code 71; the application remains usable |
| hang | Dispatcher stops responding; heartbeat supervision fails the worker |
| throw | C++ exception becomes a callback error inside the worker boundary |
| flood | At least 9,900 of 10,000 log attempts are rejected by the bounded rate limit |
| wrong-thread | A background-thread log call is rejected with state error |
| invalid-handle | Host rejects a fabricated panel handle; no panel is affected |
| stale-write | Reject a forged Apply with the wrong project generation; no calibration changes |
| slow-consumer | Delay sample callbacks; report gaps while host polling continues |
| malformed-model | Host rejects an orphan node; no invalid definition is installed |

Inspect the plugin state and diagnostics after each command. Restart manually
after a terminal failure. Reopening the panel must not repeat the selected fault.
Keep an existing host editor and Mock ECU connection active while checking that
the application continues to work.

The host's calibration regression additionally checks stale proposals, conflicts
and no write replay after worker replacement; its read/load regression checks
visible gaps and polling under slow consumers. Those tests are separate from the
basic development checker and must be included in release acceptance.

---

<a id="manual-chapter-29"></a>

Source: `examples/fuel-analysis-demo/README.md`

## fuel-analysis-demo

![Fuel analysis preview from the supplied lambda log](https://www.sgkembedded.com/epictuner/sdk/examples/fuel-analysis-demo/screenshot.png)

SDK-only C++20 example, with no Qt or private application headers. The shared
implementation is `../common/calibration.hpp`. Build from the exported SDK:

```sh
cmake --preset release
cmake --build --preset release
```

Alternatively pass `-DCMAKE_PREFIX_PATH=/absolute/path/to/sdk` to a normal CMake
configure. Start the host with `--plugin-development --plugin-manifest` followed
by the absolute generated `build/plugin.json` path. The Plugins menu opens the
panel. Safe mode overrides development opt-in. For a local package, place the
built library and generated manifest together; preserve the manifest entry name.
For signed installation, use the [test catalog](#manual-chapter-8) or
the [publishing guide](#manual-chapter-16).

Create an isolated project from `../calibration-workbench/synthetic.ini`; select Mock or work offline. The workbench tabs
show the field/table/curve/surface, channels, datalog and plugin dataset prefabs.
Refresh snapshots the current revision. Open fixtures/lambda.mlg in the Datalog tab, return to Preview and press Analyze. Set target lambda and maximum correction (1..10 percent). The bounded background calculation proposes a table correction; it supports cancellation and requires offline or Mock mode.

Acceptance: the fixture is seeded to 100 percent fuel by the application test.
The supplied 128-sample lambda log has mean 1.05, so target 1.00 yields 105 percent cells after explicit Apply. Missing/invalid lambda data is rejected. A changed tune revision requires refresh and recomputation. The demo has no Burn capability.

Follow the steps above to exercise the native panel and supplied fixtures.
The host-maintainer acceptance suite additionally checks mock writes/readback,
conflicts and saved calibration. These software fixtures do not establish
physical ECU certification.

---

<a id="manual-chapter-30"></a>

Source: `examples/hello-panels/README.md`

## Hello panels

![Native hello panel using generated example data](https://www.sgkembedded.com/epictuner/sdk/examples/hello-panels/screenshot.png)

Build native UI with EpicTuner controls: sections, form fields, bindings, tabs,
tables, charts and independent windows. The plugin source is [hello.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/hello-panels/hello.cpp).

### Build and run

Set `EPICTUNER_SDK_ROOT` to the extracted SDK root, then run from this directory:

```sh
cmake --preset sdk
cmake --build --preset sdk
epictuner --plugin-development --plugin-manifest /absolute/path/out/plugin.json
```

Use the `.exe` application name on Windows. Copy the `assets` directory with the
library/manifest if moving the build. For a signed package, follow
[Publishing](#manual-chapter-16) or build the [test catalog](#manual-chapter-8).

### Acceptance

Open **Plugins → Examples → Hello panels**. Edit Name and Target lambda using
Enter and Escape; change Mode and the checkbox. Click Say hello. The status
changes only on that action. The mirrored field shares the value.

Open Inspector A twice: it focuses the same instance. Open Inspector B and
give each a different note. Close and reopen them; instance state stays separate.
Open the modal dialog and dismiss it with Done. Change theme/scale, move a
floating panel and resize it. Restart the application with the same development
manifest and settings: opted-in values and placement restore without replaying
the greeting action. Invalid edits keep the last valid value.

All data is generated in [hello.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/hello-panels/hello.cpp); no ECU or external fixture is
required. [UI.md](#manual-chapter-22) describes the node/property contract.

---

<a id="manual-chapter-31"></a>

Source: `examples/imgui-inspector/README.md`

## ImGui inspector

This compiled example opens a GLFW/OpenGL/Dear ImGui window in its own EpicTuner
plugin worker. It uses the public SDK for an ET panel, project/channel reads and
the independent window lifecycle. It does not link Qt or private app classes.

Configure from an extracted SDK (CMake 3.24+, C++20, OpenGL, GLFW's platform
development headers):

```sh
cmake -S examples/imgui-inspector -B out/imgui -DCMAKE_PREFIX_PATH=/absolute/sdk
cmake --build out/imgui --config Release
```

The included `release` CMake preset configures the same example using the
extracted SDK root two directories above. Set `CC` and `CXX` before configuring
if more than one native compiler is installed.

![Native inspector with labeled synthetic preview before connection](https://www.sgkembedded.com/epictuner/sdk/examples/imgui-inspector/screenshot.png)

Run `epictuner-plugin-check --development --manifest out/imgui/plugin.json
--custom-window --ui-panel org.epictuner.examples.imgui.main`, or start the
application with `--plugin-development --plugin-manifest /absolute/out/imgui/plugin.json`.
Open the example from the Plugins menu and use its buttons to show, focus, close
and reopen the native window. Create a Mock ECU project to see live samples.
The demo searches paged project metadata for RPM, MAP and lambda channels,
including rusEFI's `RPMValue`, `MAPValue` and `RealLambdaValue1` names.

The demo uses an unsigned local development manifest. To distribute it, sign a
package using `out/imgui/package-manifest.json` after replacing the local test
publisher identity and matching it to your own Ed25519 public/private key.
Copy the library to `payload/<platform>/imgui-inspector.<dll-or-so>` and the two
bundled license files to `payload/licenses/`, then use
`python tools/package.py --manifest out/imgui/package-manifest.json
--payload-root payload --key /outside/sdk/publisher.pem --output inspector.etplugin`.
See [PACKAGE.md](#manual-chapter-14) for trust and installation.
Both bundled native dependencies retain their upstream license files.

---

<a id="manual-chapter-32"></a>

Source: `examples/licensed-feature-demo/README.md`

## Licensed feature demo

This compiled C++ plugin is built with the exported SDK and no Qt or private
application headers. Its ET panel has a free action, an entitlement-gated paid
action, and an Activate action that opens the host plugin manager. The paid
action asks `ET_SERVICE_LICENSE` asynchronously for
`org.epictuner.feature.pro`; the host checks signed entitlement status at
dispatch. The sample is intentionally transparent and is not DRM.

Build with a native 64-bit compiler:

```sh
cmake -S examples/licensed-feature-demo -B out -DCMAKE_PREFIX_PATH=/path/to/EpicTunerSDK
cmake --build out --config Release
```

For a signed test package, edit `out/package-manifest.json` with your test
publisher/key IDs. Place the resulting library at
`payload/<platform>/licensed-feature-demo.<dll-or-so>`, then run:

```sh
python tools/package.py --manifest out/package-manifest.json --payload-root payload \
  --key /outside/sdk/test-publisher.pem --output licensed-feature.etplugin
```

`tools/package.py` requires Python `cryptography` and `jsonschema` in the
publisher environment. Private keys stay outside the SDK and package. Import
the corresponding publisher public-key JSON in **Plugins → Trust publisher
key**, then install the package. For license testing, import an issuer public
key separately and either use the manager's online activation URL or export a
challenge, sign it with `tools/test-issuer.py`, and import its response. The
test issuer accepts an external Ed25519 PEM and is not a production service.

The native acceptance run captured [the panel](https://www.sgkembedded.com/epictuner/sdk/examples/licensed-feature-demo/screenshot.png) after a signed
install and both online and offline mock activation. It checks that the paid
button is denied before activation and accepted after it. See
`docs/LICENSE.md` and `docs/PACKAGE.md` for the wire and trust rules.

---

<a id="manual-chapter-33"></a>

Source: `examples/lifecycle/README.md`

## C, C++ and background lifecycle

These small plugins demonstrate the versioned entry point, allocator ownership,
callback exception boundary and cancellable worker jobs.

### Build

Set `EPICTUNER_SDK_ROOT` and use `cmake --preset sdk` followed by
`cmake --build --preset sdk` in this directory. The build produces C and C++
libraries plus development manifests.

```sh
epictuner-plugin-check --development --manifest out/plugin.json
epictuner-plugin-check --development --manifest out/plugin-c.json
epictuner-plugin-check --development --manifest out/plugin-background.json --expect-log "background sum=55"
```

Windows executable names end in `.exe`. A successful checker run covers startup,
ordered event delivery, heartbeat and stop. The background example computes a
synthetic sum and returns it through the dispatcher. These are nonvisual ABI
examples, additional to the eight gallery examples.

See [lifecycle.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/lifecycle/lifecycle.cpp), [lifecycle.c](https://www.sgkembedded.com/epictuner/sdk/examples/lifecycle/lifecycle.c) and
[background.cpp](https://www.sgkembedded.com/epictuner/sdk/examples/lifecycle/background.cpp). Run the exported conformance tests to check C
and C++ shared-module negotiation, including incompatible-version rejection.

---

<a id="manual-chapter-34"></a>

Source: `examples/live-telemetry/README.md`

## live-telemetry

![Native live telemetry with synthetic Mock ECU samples](https://www.sgkembedded.com/epictuner/sdk/examples/live-telemetry/screenshot.png)

Build against the extracted SDK 1.0 with CMake 3.24+ and a native C++20 compiler.
No Qt or private application headers are required.

```sh
cmake -S . -B out -G Ninja -DCMAKE_PREFIX_PATH=/absolute/path/to/sdk
cmake --build out
```

Or set `EPICTUNER_SDK_ROOT`, then run `cmake --preset sdk` and
`cmake --build --preset sdk`. Windows needs an initialized MinGW or x64 MSVC
environment. CMake generates a manifest for the selected OS/architecture.

Development package recipe: copy `out/live-telemetry.dll` (Linux: `live-telemetry.so`) and
`out/plugin.json` into one directory. Launch EpicTuner with
`--plugin-development --plugin-manifest /absolute/path/plugin.json`.
Open the example under **Plugins > Examples**. These unsigned packages require
explicit opt-in on each launch. For signed installation, follow [Publishing](#manual-chapter-16).
Never include private tunes or INIs.

See [READ.md](#manual-chapter-17) for API contracts and bounds.
The **Host prefabs** tab includes the channel picker and live graph prefab.

Create a temporary project from `synthetic.ini`, choose Mock and a 100 Hz request
rate, then connect. The picker pages through 16 channels; the readout/graph report
timestamps, units and skipped samples. RPM publishes a namespaced compiled RPM/2
channel. Disconnect/reconnect, close/reopen and switch channels. Closed panels
release subscriptions. Display coalescing intentionally reports gaps. No ECU writes.

---

<a id="manual-chapter-35"></a>

Source: `examples/log-analyzer/README.md`

## log-analyzer

![Native statistics for the synthetic comparison logs](https://www.sgkembedded.com/epictuner/sdk/examples/log-analyzer/screenshot.png)

Build against the extracted SDK 1.0 with CMake 3.24+ and a native C++20 compiler.
No Qt or private application headers are required.

```sh
cmake -S . -B out -G Ninja -DCMAKE_PREFIX_PATH=/absolute/path/to/sdk
cmake --build out
```

Or set `EPICTUNER_SDK_ROOT`, then run `cmake --preset sdk` and
`cmake --build --preset sdk`. Windows needs an initialized MinGW or x64 MSVC
environment. CMake generates a manifest for the selected OS/architecture.

Development package recipe: copy `out/log-analyzer.dll` (Linux: `log-analyzer.so`) and
`out/plugin.json` into one directory. Launch EpicTuner with
`--plugin-development --plugin-manifest /absolute/path/plugin.json`.
Open the example under **Plugins > Examples**. These unsigned packages require
explicit opt-in on each launch. For signed installation, follow [Publishing](#manual-chapter-16).
Never include private tunes or INIs.

See [READ.md](#manual-chapter-17) for API contracts and bounds.
The **Datalog browser** tab embeds the host datalog prefab.

Open `fixtures/known-0.mlg` and `fixtures/known-1.mlg` in EpicTuner. Refresh the
plugin, select the first log and Signal, then Compare. Expected results:

| Log | Count | Mean | Min | Max |
|---|---:|---:|---:|---:|
| known-0 | 256 | 127.5 | 0 | 255 |
| known-1 | 256 | 227.5 | 100 | 355 |

These deterministic MlgWriter fixtures have rows 0..255, Time=row/100 and
Signal=row (second file adds 100). They contain no captured data. Choose a
first/last row for range analysis. Compare matches the selected channel name in
the next open log and rejects missing channels. Cancel stops further requests
and cancels the current background job. The plugin keeps one bounded batch and
running statistics. The demonstration pickers show the first 32 logs/columns;
the API supports paging beyond that limit.

---

<a id="manual-chapter-36"></a>

Source: `templates/panel/README.md`

## Starter panel

Set `EPICTUNER_SDK_ROOT` to the extracted SDK directory. In an x64 MinGW or
MSVC developer shell (or a Linux native compiler shell):

```sh
cmake --preset sdk
cmake --build --preset sdk
epictuner-plugin-check --development --manifest out/plugin.json --ui-panel org.example.starter.main
epictuner --plugin-development --plugin-manifest /absolute/path/out/plugin.json
```

On Windows use the `.exe` executable names. Open **Plugins → Examples → Starter
panel**. Click **Say hello** and observe the updated label. Edit Name, close the
panel, and reopen it to verify persistence. The handler sends an atomic patch
with the event revision; completion errors are visible in plugin diagnostics.

Change the identity in both `plugin.cpp` and `plugin.json.in` when copying this
template by hand. `tools/new-plugin.py` makes those changes consistently.
Keep callback owners alive until worker shutdown drains callbacks. Service calls
belong on the dispatcher thread. Use the jobs API for lengthy computation.

For signed distribution follow the SDK's `docs/PUBLISHING.md`. The generated
development manifest is intentionally unsigned and only works with explicit
development launch options. No external redistribution license has been selected.

---

<a id="manual-appendices"></a>

## Public headers, schemas and complete examples

### include/epictuner/contracts.h

````cpp
#ifndef EPICTUNER_CONTRACTS_H
#define EPICTUNER_CONTRACTS_H
#include <stddef.h>
#include <stdint.h>

/* Stable identifiers and common data contracts. The supported mask below is
 * the implemented subset; other known IDs remain reserved. */
#define ET_CAP_LOG UINT64_C(1)
#define ET_CAP_EVENTS UINT64_C(2)
#define ET_CAP_UI UINT64_C(4)
#define ET_CAP_PROJECT_READ UINT64_C(8)
#define ET_CAP_CHANNEL_READ UINT64_C(16)
#define ET_CAP_DATALOG_READ UINT64_C(32)
#define ET_CAP_SETTINGS UINT64_C(64)
#define ET_CAP_JOBS UINT64_C(128)
#define ET_CAP_TUNE_READ UINT64_C(256)
#define ET_CAP_TUNE_PROPOSE UINT64_C(512)
#define ET_CAP_TUNE_APPLY UINT64_C(1024)
#define ET_CAP_TUNE_BURN UINT64_C(2048)
#define ET_CAP_FILES UINT64_C(4096)
#define ET_CAP_LICENSE UINT64_C(8192)
#define ET_CAP_CUSTOM_WINDOW UINT64_C(16384)
#define ET_KNOWN_CAPABILITIES UINT64_C(32767)
#define ET_SUPPORTED_CAPABILITIES (ET_CAP_LOG | ET_CAP_EVENTS | ET_CAP_JOBS | ET_CAP_UI | ET_CAP_PROJECT_READ | ET_CAP_CHANNEL_READ | ET_CAP_DATALOG_READ | ET_CAP_SETTINGS | ET_CAP_TUNE_READ | ET_CAP_TUNE_PROPOSE | ET_CAP_TUNE_APPLY | ET_CAP_TUNE_BURN | ET_CAP_LICENSE | ET_CAP_CUSTOM_WINDOW)

#define ET_SERVICE_CONTEXT UINT32_C(1)
#define ET_SERVICE_UI UINT32_C(2)
#define ET_SERVICE_PROJECT UINT32_C(3)
#define ET_SERVICE_CHANNELS UINT32_C(4)
#define ET_SERVICE_DATALOG UINT32_C(5)
#define ET_SERVICE_SETTINGS UINT32_C(6)
#define ET_SERVICE_JOBS UINT32_C(7)
#define ET_SERVICE_TUNE UINT32_C(8)
#define ET_SERVICE_FILES UINT32_C(9)
#define ET_SERVICE_LICENSE UINT32_C(10)
#define ET_SERVICE_CUSTOM_WINDOW UINT32_C(11)
#define ET_SERVICE_VERSION_1 UINT32_C(0x00010000)

#define ET_MAX_STRING_BYTES UINT32_C(4096)
#define ET_MAX_ERROR_BYTES UINT32_C(512)
#define ET_MAX_FRAME_BYTES UINT32_C(65536)
#define ET_MAX_QUEUED_BYTES UINT32_C(262144)
#define ET_MAX_PENDING_REQUESTS UINT32_C(32)
#define ET_MAX_UI_NODES UINT32_C(4096)
#define ET_MAX_UI_DEPTH UINT32_C(32)
#define ET_MAX_PANELS UINT32_C(32)
#define ET_MAX_PANEL_INSTANCES UINT32_C(64)
#define ET_MAX_SUBSCRIPTIONS UINT32_C(64)
#define ET_MAX_JOBS UINT32_C(16)
#define ET_MAX_UI_HZ UINT32_C(30)
#define ET_MAX_STREAM_HZ UINT32_C(100)
#define ET_MAX_LOG_HZ UINT32_C(100)
#define ET_MAX_MESSAGE_HZ UINT32_C(200)
#define ET_STARTUP_TIMEOUT_MS UINT32_C(3000)
#define ET_OPERATION_TIMEOUT_MS UINT32_C(3000)
#define ET_HEARTBEAT_INTERVAL_MS UINT32_C(500)
#define ET_SHUTDOWN_TIMEOUT_MS UINT32_C(1000)

typedef int32_t et_result;
#define ET_OK ((et_result)0)
#define ET_ERROR_ARGUMENT ((et_result)1)
#define ET_ERROR_ABI ((et_result)2)
#define ET_ERROR_CAPABILITY ((et_result)3)
#define ET_ERROR_LIMIT ((et_result)4)
#define ET_ERROR_STATE ((et_result)5)
#define ET_ERROR_CALLBACK ((et_result)6)
#define ET_ERROR_UNSUPPORTED ((et_result)7)
#define ET_ERROR_STALE ((et_result)8)
#define ET_ERROR_CANCELLED ((et_result)9)
#define ET_ERROR_TIMEOUT ((et_result)10)
#define ET_ERROR_PROTOCOL ((et_result)11)
#define ET_ERROR_CONFLICT ((et_result)12)
#define ET_ERROR_NOT_FOUND ((et_result)13)
#define ET_ERROR_IO ((et_result)14)
#define ET_ERROR_BUSY ((et_result)15)
#define ET_ERROR_LAST ET_ERROR_BUSY

#define ET_HANDLE_PANEL UINT32_C(1)
#define ET_HANDLE_NODE UINT32_C(2)
#define ET_HANDLE_PROJECT UINT32_C(3)
#define ET_HANDLE_SUBSCRIPTION UINT32_C(4)
#define ET_HANDLE_JOB UINT32_C(5)
#define ET_HANDLE_FILE UINT32_C(6)
#define ET_HANDLE_PROPOSAL UINT32_C(7)
#define ET_VALUE_NULL UINT32_C(0)
#define ET_VALUE_BOOL UINT32_C(1)
#define ET_VALUE_I64 UINT32_C(2)
#define ET_VALUE_U64 UINT32_C(3)
#define ET_VALUE_F64 UINT32_C(4)
#define ET_VALUE_STRING UINT32_C(5)
#define ET_VALUE_BYTES UINT32_C(6)
#define ET_VALUE_HANDLE UINT32_C(7)
#define ET_PLACEMENT_WORKSPACE UINT32_C(1)
#define ET_PLACEMENT_FLOATING UINT32_C(2)
#define ET_PLACEMENT_MODAL UINT32_C(4)
#define ET_OPERATION_QUEUED UINT32_C(1)
#define ET_OPERATION_RUNNING UINT32_C(2)
#define ET_OPERATION_COMMITTED UINT32_C(3)
#define ET_OPERATION_COMPLETE UINT32_C(4)
#define ET_FEATURE_AVAILABLE UINT32_C(1)

#pragma pack(push, 8)
typedef struct et_string {
    const char* data;
    uint32_t size;
    uint32_t reserved;
} et_string;
typedef struct et_bytes {
    const uint8_t* data;
    uint32_t size;
    uint32_t reserved;
} et_bytes;
typedef struct et_handle {
    uint64_t session;
    uint64_t project_generation; /* 0 = worker-global */
    uint32_t slot;              /* 0 = invalid; never a pointer */
    uint32_t generation;        /* increments on slot reuse; never wraps */
    uint32_t kind;
    uint32_t reserved;
} et_handle;
typedef struct et_value {
    uint32_t kind;
    uint32_t reserved;
    union {
        uint64_t boolean;
        int64_t i64;
        uint64_t u64;
        double f64;             /* finite IEEE 754 binary64 only */
        et_string string;
        et_bytes bytes;
        et_handle handle;
    } data;
} et_value;
typedef struct et_error {
    uint32_t struct_size;
    et_result code;
    uint64_t operation_id;       /* 0 = immediate call */
    uint32_t detail_size;        /* UTF-8 bytes, excludes terminator */
    uint32_t reserved;
    char detail[ET_MAX_ERROR_BYTES]; /* caller-owned, NUL terminated */
} et_error;
typedef struct et_completion {
    uint32_t struct_size;
    et_result code;
    uint64_t operation_id;
    uint64_t project_generation;
    et_value value;              /* borrowed only during completion callback */
    et_string detail;            /* borrowed only during completion callback */
} et_completion;
typedef struct et_feature_info {
    uint32_t struct_size;
    uint32_t service_version;
    uint64_t required_capabilities;
    uint32_t flags;
    uint32_t reserved;
} et_feature_info;
typedef struct et_limits {
    uint32_t struct_size;
    uint32_t version;
    uint32_t frame_bytes, queued_bytes, pending_requests, string_bytes;
    uint32_t ui_nodes, ui_depth, panels, panel_instances, subscriptions, jobs;
    uint32_t ui_hz, stream_hz, log_hz, message_hz;
    uint32_t startup_ms, operation_ms, heartbeat_ms, shutdown_ms;
} et_limits;
#pragma pack(pop)

#if defined(__cplusplus)
# define ET_LAYOUT_ASSERT(c) static_assert(c, #c)
#else
# define ET_LAYOUT_ASSERT(c) _Static_assert(c, #c)
#endif
ET_LAYOUT_ASSERT(sizeof(void*) == 8);
ET_LAYOUT_ASSERT(sizeof(et_string) == 16 && offsetof(et_string, size) == 8);
ET_LAYOUT_ASSERT(sizeof(et_bytes) == 16);
ET_LAYOUT_ASSERT(sizeof(et_handle) == 32 && offsetof(et_handle, slot) == 16);
ET_LAYOUT_ASSERT(sizeof(et_value) == 40 && offsetof(et_value, data) == 8);
ET_LAYOUT_ASSERT(sizeof(et_error) == 536 && offsetof(et_error, detail) == 24);
ET_LAYOUT_ASSERT(sizeof(et_completion) == 80 && offsetof(et_completion, value) == 24);
ET_LAYOUT_ASSERT(sizeof(et_feature_info) == 24 && sizeof(et_limits) == 80);
#endif
````

### include/epictuner/license.h

````cpp
#ifndef EPICTUNER_LICENSE_H
#define EPICTUNER_LICENSE_H
#include <epictuner/read.h>
/* ET_SERVICE_LICENSE v1 uses the same bounded asynchronous request/reply ABI
 * as read services. STATUS returns compact JSON in reply.text; CHALLENGE
 * returns an offline challenge. ACTIVATE asks the host to show its activation
 * UI; DEACTIVATE clears the local cached entitlement. Results arrive in the
 * callback and are never synchronous with the request call. */
#define ET_LICENSE_STATUS 40u
#define ET_LICENSE_CHALLENGE 41u
#define ET_LICENSE_ACTIVATE 42u
#define ET_LICENSE_DEACTIVATE 43u
typedef et_read_service et_license_service;
#endif
````

### include/epictuner/plugin.h

````cpp
#ifndef EPICTUNER_PLUGIN_H
#define EPICTUNER_PLUGIN_H

/* M0 ABI 1.1; Windows x64 and Linux x64/ARM64. macOS is a future target.
 * All strings are UTF-8 byte spans, borrowed for the duration of the call.
 * Service functions copy input before return. Never transfer allocator ownership.
 * See docs/CONTRACT.md for threading, negotiation, and wire rules. */
#include <stddef.h>
#include <stdint.h>
#include <epictuner/contracts.h>

#if defined(_WIN32)
# define ET_CALL __cdecl
# define ET_EXPORT __declspec(dllexport)
#else
# define ET_CALL
# define ET_EXPORT __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif

#define ET_ABI_VERSION UINT32_C(0x00010001)
#define ET_ABI_MAJOR(version) ((uint32_t)(version) >> 16)
#define ET_ABI_MINOR(version) ((uint32_t)(version) & UINT32_C(0xffff))
#define ET_LOG_INFO UINT32_C(1)
#define ET_LOG_WARNING UINT32_C(2)
#define ET_LOG_ERROR UINT32_C(3)
/* Developer message event. UI/project event types will be negotiated separately. */
#define ET_EVENT_MESSAGE UINT32_C(1)

#pragma pack(push, 8)
typedef struct et_query {
    uint32_t struct_size;
    uint32_t abi_version;
    uint64_t capabilities;
} et_query;

typedef struct et_host_api {
    uint32_t struct_size;
    uint32_t abi_version;
    uint64_t capabilities;
    void* context;
    /* Dispatcher thread only; bounded enqueue, never waits for the GUI. */
    et_result (ET_CALL *log)(void* context, uint32_t level, et_string message);
    /* ABI 1.1 optional tail. v1.0 callers use the 32-byte prefix only. */
    et_result (ET_CALL *get_service)(void* context, uint32_t service_id,
        uint32_t requested_version, void* table, uint32_t table_size, et_error* error);
} et_host_api;
#define ET_HOST_API_V1_SIZE UINT32_C(32)

typedef struct et_context_service {
    uint32_t struct_size;
    uint32_t version;
    void* context;
    et_result (ET_CALL *get_limits)(void* context, et_limits* limits);
    et_result (ET_CALL *get_feature)(void* context, et_string name, et_feature_info* feature);
} et_context_service;

typedef void (ET_CALL *et_completion_fn)(void* context, const et_completion* completion);
typedef struct et_async_options {
    uint32_t struct_size;
    uint32_t timeout_ms; /* 1..ET_OPERATION_TIMEOUT_MS, monotonic host clock */
    uint32_t flags;      /* zero in v1 */
    uint32_t reserved;
    void* context;      /* plugin-owned through terminal callback or stop */
    et_completion_fn complete; /* worker dispatcher; never transported over IPC */
} et_async_options;

/* M1 jobs: only work runs on a background thread. The cancellation probe is
 * thread safe. Output is worker-owned; write at most capacity bytes and set size.
 * Completion runs on the dispatcher after work returns, with borrowed bytes.
 * Contexts must live until completion. Stop cancels/drains before plugin.stop. */
typedef struct et_job_control {
    void* context;
    uint32_t (ET_CALL *is_cancelled)(void* context);
} et_job_control;
typedef struct et_job_output {
    uint8_t* data;
    uint32_t capacity;
    uint32_t size;
} et_job_output;
typedef et_result (ET_CALL *et_job_fn)(void* context,
    const et_job_control* control, et_job_output* output);
typedef struct et_jobs_service {
    uint32_t struct_size;
    uint32_t version;
    void* context;
    et_result (ET_CALL *submit)(void* context, et_job_fn work, void* work_context,
        const et_async_options* options, et_handle* job);
    et_result (ET_CALL *cancel)(void* context, et_handle job);
} et_jobs_service;

typedef struct et_event {
    uint32_t struct_size;
    uint32_t type;
    uint64_t operation_id;
    uint64_t project_generation; /* Zero for worker-global events in SDK 0.2. */
    et_string text;
} et_event;

typedef struct et_plugin_api {
    /* Caller supplies capacity. Plugin writes only the negotiated v1 prefix. */
    uint32_t struct_size;
    uint32_t abi_version;
    uint64_t required_capabilities;
    et_string plugin_id; /* Immutable plugin-owned storage until process exit. */
    /* Host table lives through stop. On failed start, instance MUST be null. */
    et_result (ET_CALL *start)(const et_host_api* host, void** instance);
    et_result (ET_CALL *stop)(void* instance);
    et_result (ET_CALL *on_event)(void* instance, const et_event* event);
} et_plugin_api;
#pragma pack(pop)

ET_LAYOUT_ASSERT(offsetof(et_host_api, get_service) == 32);
ET_LAYOUT_ASSERT(sizeof(et_context_service) == 32);
ET_LAYOUT_ASSERT(sizeof(et_job_control) == 16 && sizeof(et_job_output) == 16);
ET_LAYOUT_ASSERT(sizeof(et_jobs_service) == 32);
ET_LAYOUT_ASSERT(sizeof(et_async_options) == 32 && offsetof(et_async_options, complete) == 24);

typedef et_result (ET_CALL *et_plugin_query_fn)(const et_query*, et_plugin_api*);
/* Define/export a function named et_plugin_query with the signature above.
 * No prototype is exported from this shared header: a host consumer must not
 * acquire dllexport linkage. See ET_PLUGIN or the C lifecycle example. */

#ifdef __cplusplus
}
static_assert(sizeof(void*) == 8, "EpicTuner preview SDK requires a 64-bit target");
static_assert(sizeof(et_string) == 16 && offsetof(et_string, size) == 8);
static_assert(sizeof(et_query) == 16 && sizeof(et_host_api) == 40);
static_assert(sizeof(et_event) == 40 && offsetof(et_event, text) == 24);
static_assert(sizeof(et_plugin_api) == 56 && offsetof(et_plugin_api, start) == 32);
#else
_Static_assert(sizeof(void*) == 8, "EpicTuner preview SDK requires a 64-bit target");
_Static_assert(sizeof(et_string) == 16 && offsetof(et_string, size) == 8, "string layout");
_Static_assert(sizeof(et_query) == 16 && sizeof(et_host_api) == 40, "host layout");
_Static_assert(sizeof(et_event) == 40 && offsetof(et_event, text) == 24, "event layout");
_Static_assert(sizeof(et_plugin_api) == 56 && offsetof(et_plugin_api, start) == 32, "plugin layout");
#endif
#endif
````

### include/epictuner/read.h

````cpp
#ifndef EPICTUNER_READ_H
#define EPICTUNER_READ_H
#include <epictuner/plugin.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Optional project/channels/datalog/settings v1 tables. See docs/READ.md.
 * All calls and callbacks use the worker dispatcher. Inputs are copied before
 * return; reply strings/items are borrowed only until callback returns. */
#define ET_READ_PROJECT 1u
#define ET_READ_CHANNELS 2u
#define ET_READ_SNAPSHOT 3u
#define ET_READ_SUBSCRIBE 4u
#define ET_READ_UNSUBSCRIBE 5u
#define ET_READ_DERIVE 6u
#define ET_READ_PUBLISH 7u
#define ET_READ_LOGS 8u
#define ET_READ_LOG_CHANNELS 9u
#define ET_READ_LOG_BATCH 10u
#define ET_READ_SETTING_GET 11u
#define ET_READ_SETTING_SET 12u
#define ET_READ_LOG_SELECT 13u
#define ET_READ_VALID 1u
#define ET_READ_STALE 2u
#define ET_READ_CONNECTED 4u
#define ET_READ_DERIVED 8u
#define ET_READ_LATEST 1u
#define ET_READ_PROJECT_SETTING 2u
#define ET_READ_EVENT_PROJECT 1u
#define ET_READ_EVENT_SAMPLES 2u
#define ET_READ_MAX_CHANNELS 16u
#define ET_READ_MAX_ROWS 32u
#define ET_READ_MAX_ITEMS 512u
#define ET_READ_RING_SAMPLES 128u
#define ET_READ_MAX_SETTINGS 64u
#pragma pack(push, 8)
typedef struct et_read_item {
    et_string id, label, units;
    double value, timestamp;
    uint64_t sequence;
    uint32_t flags, reserved;
} et_read_item;
typedef struct et_read_request {
    uint32_t struct_size, operation;
    uint64_t project_generation;
    et_handle handle;
    uint32_t offset, count, flags, schema;
    et_string id, text;
    const et_string* channels;
    uint32_t channel_count, reserved;
    double value, timestamp;
} et_read_request;
typedef struct et_read_reply {
    uint32_t struct_size;
    et_result code;
    uint64_t operation_id, project_generation;
    et_handle handle;
    uint32_t total, offset, gaps, schema, flags, event;
    et_string text;
    const et_read_item* items;
    uint32_t item_count, reserved;
} et_read_reply;
typedef et_result (ET_CALL *et_read_callback)(void* context, const et_read_reply* reply);
typedef struct et_read_service {
    uint32_t struct_size, version;
    void* context;
    et_result (ET_CALL *request)(void*, const et_read_request*, et_read_callback, void*, uint64_t* operation);
    et_result (ET_CALL *cancel)(void*, uint64_t operation);
    et_result (ET_CALL *listen)(void*, et_read_callback, void*);
} et_read_service;
#pragma pack(pop)
ET_LAYOUT_ASSERT(sizeof(et_read_item) == 80);
ET_LAYOUT_ASSERT(sizeof(et_read_request) == 128);
ET_LAYOUT_ASSERT(sizeof(et_read_reply) == 112);
ET_LAYOUT_ASSERT(sizeof(et_read_service) == 40);
#ifdef __cplusplus
}
#endif
#endif
````

### include/epictuner/tune.h

````cpp
#ifndef EPICTUNER_TUNE_H
#define EPICTUNER_TUNE_H
#include <epictuner/read.h>
/* et.tune.v1 uses the bounded async request/reply table. See TUNE.md for the
 * exact field mapping and TSV proposal encoding. No pointers cross IPC. */
#define ET_TUNE_CATALOG 20u
#define ET_TUNE_SNAPSHOT 21u
#define ET_TUNE_PROPOSE 22u
#define ET_TUNE_APPLY 23u
#define ET_TUNE_UNDO 24u
#define ET_TUNE_REDO 25u
#define ET_TUNE_STATUS 26u
#define ET_TUNE_BURN 27u
#define ET_TUNE_DISCARD 28u
#define ET_TUNE_LOCAL_ACCEPTED 16u
#define ET_TUNE_RAM_UNVERIFIED 32u
#define ET_TUNE_OFFLINE 64u
#define ET_TUNE_FLASH_PENDING 128u
#define ET_TUNE_READ_ONLY 256u
#define ET_TUNE_RAM_VERIFIED 512u
#define ET_TUNE_RAM_FAILED 1024u
#define ET_TUNE_MOCK 2048u
#define ET_TUNE_MAX_EDITS 64u
typedef et_read_service et_tune_service;
#endif
````

### include/epictuner/ui.h

````cpp
#ifndef EPICTUNER_UI_H
#define EPICTUNER_UI_H
#include <epictuner/plugin.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Optional et.ui.v1 service; no change to the ABI 1.0/1.1 prefixes.
 * All spans are copied before return. All callbacks run on the dispatcher.
 * ET_OK means queued; completion is the authoritative host acceptance. */
#define ET_UI_COLUMN 1u
#define ET_UI_ROW 2u
#define ET_UI_GRID 3u
#define ET_UI_SECTION 4u
#define ET_UI_TABS 5u
#define ET_UI_SPLITTER 6u
#define ET_UI_SCROLL 7u
#define ET_UI_LABEL 8u
#define ET_UI_BUTTON 9u
#define ET_UI_TEXT 10u
#define ET_UI_NUMBER 11u
#define ET_UI_CHECKBOX 12u
#define ET_UI_SWITCH 13u
#define ET_UI_SELECT 14u
#define ET_UI_SLIDER 15u
#define ET_UI_SEPARATOR 16u
#define ET_UI_PROGRESS 17u
#define ET_UI_BANNER 18u
#define ET_UI_IMAGE 19u
#define ET_UI_LIST 20u
#define ET_UI_TABLE 21u
#define ET_UI_TREE 22u
#define ET_UI_CHART 23u
#define ET_UI_FORM 24u
#define ET_UI_FIELD_EDITOR 25u
#define ET_UI_CALIBRATION_TABLE 26u
#define ET_UI_CURVE_EDITOR 27u
#define ET_UI_TABLE_SURFACE 28u
#define ET_UI_CHANNEL_PICKER 29u
#define ET_UI_LIVE_PANEL 30u
#define ET_UI_DATALOG_PANEL 31u
#define ET_UI_ANALYSIS_PANEL 32u

#define ET_UI_TITLE 1u
#define ET_UI_VALUE 2u
#define ET_UI_SELECTED_INDEX ET_UI_VALUE /* select PROPERTY: F64 zero-based index, -1 = no selection */
#define ET_UI_ENABLED 3u
#define ET_UI_VISIBLE 4u
#define ET_UI_MINIMUM 5u
#define ET_UI_MAXIMUM 6u
#define ET_UI_STEP 7u
#define ET_UI_DECIMALS 8u
#define ET_UI_UNITS 9u
#define ET_UI_OPTIONS 10u /* newline-delimited strings; empty = no options yet (host >= 0.8.3) */
#define ET_UI_TOOLTIP 11u
#define ET_UI_ERROR 12u
#define ET_UI_BIND_VALUE 13u /* another node's value; same instance only */
#define ET_UI_BIND_ENABLED 14u /* boolean node value */
#define ET_UI_BIND_VISIBLE 15u /* boolean node value */
#define ET_UI_TWO_WAY 16u
#define ET_UI_PERSIST 17u /* opt in to saving value */
#define ET_UI_MIN_WIDTH 18u
#define ET_UI_PREFERRED_WIDTH 19u
#define ET_UI_MAX_WIDTH 20u
#define ET_UI_FILL 21u
#define ET_UI_COLUMNS 22u
#define ET_UI_DATA 23u /* bounded tab/newline delimited data; chart: newline numbers */
#define ET_UI_SOURCE 24u /* adjacent package PNG/JPG asset */
#define ET_UI_HELP 25u /* https link, opened only by user */
#define ET_UI_READ_ONLY 26u
#define ET_UI_MIN_HEIGHT 27u
#define ET_UI_PREFERRED_HEIGHT 28u
#define ET_UI_MAX_HEIGHT 29u
#define ET_UI_STRETCH 30u
#define ET_UI_TARGET 31u /* host field/table/curve/channel ID; never a pointer */

#define ET_PANEL_MULTIPLE 1u
#define ET_PANEL_PROJECT 2u
#define ET_PANEL_PERSIST 4u
#define ET_PANEL_DIRTY_CLOSE 8u
#define ET_UI_FOCUS 1u
#define ET_UI_CLOSE 2u
#define ET_UI_ALLOW_CLOSE 3u
#define ET_UI_DENY_CLOSE 4u
#define ET_UI_CLICK 1u
#define ET_UI_PREVIEW 2u
#define ET_UI_COMMIT 3u
#define ET_UI_OPENED 4u
#define ET_UI_CLOSED 5u
#define ET_UI_CLOSE_REQUEST 6u
#define ET_UI_FOCUSED 7u
#define ET_UI_RESIZED 8u
#define ET_UI_EVENT_SELECTED 9u /* EVENT type, NOT a property ID; value is F64 zero-based index */
#define ET_UI_SELECTED ET_UI_EVENT_SELECTED /* legacy event name; never use in a property/patch */

#pragma pack(push, 8)
typedef struct et_ui_property { uint32_t id; uint32_t reserved; et_value value; } et_ui_property;
typedef struct et_ui_node {
    uint32_t struct_size, type;
    et_string id, parent;
    const et_ui_property* properties;
    uint32_t property_count, reserved;
} et_ui_node;
typedef struct et_ui_panel {
    uint32_t struct_size, flags;
    et_string id, title, menu;
    uint32_t placements, state_schema;
    uint32_t width, height, min_width, min_height;
    const et_ui_node* nodes;
    uint32_t node_count, reserved;
} et_ui_panel;
typedef struct et_ui_patch { et_string node; et_ui_property property; } et_ui_patch;
typedef struct et_ui_event {
    /* For select/list/table/tree events value is F64 index, not option text.
     * Patch a select with ET_UI_SELECTED_INDEX (ET_UI_VALUE), not ET_UI_SELECTED. */
    uint32_t struct_size, type;
    et_handle panel;
    et_string definition, instance, node;
    et_value value;
    uint32_t revision, origin;
} et_ui_event;
typedef et_result (ET_CALL *et_ui_event_fn)(void*, const et_ui_event*);
typedef struct et_ui_service {
    uint32_t struct_size, version;
    void* context;
    et_result (ET_CALL *listen)(void*, et_ui_event_fn, void*);
    et_result (ET_CALL *define_panel)(void*, const et_ui_panel*, const et_async_options*);
    et_result (ET_CALL *open)(void*, et_string definition, et_string instance,
        uint32_t placement, const et_async_options*);
    et_result (ET_CALL *patch)(void*, et_handle, const et_ui_patch*, uint32_t count,
        uint32_t expected_revision, uint32_t origin, const et_async_options*);
    et_result (ET_CALL *action)(void*, et_handle, uint32_t action,
        uint32_t origin, const et_async_options*);
} et_ui_service;
#pragma pack(pop)
ET_LAYOUT_ASSERT(sizeof(et_ui_property) == 48);
ET_LAYOUT_ASSERT(sizeof(et_ui_node) == 56);
ET_LAYOUT_ASSERT(sizeof(et_ui_panel) == 96);
ET_LAYOUT_ASSERT(sizeof(et_ui_patch) == 64);
ET_LAYOUT_ASSERT(sizeof(et_ui_event) == 136);
ET_LAYOUT_ASSERT(sizeof(et_ui_service) == 56);
#ifdef __cplusplus
}
#endif
#endif
````

### include/epictuner/window.h

````cpp
#ifndef EPICTUNER_WINDOW_H
#define EPICTUNER_WINDOW_H
#include <epictuner/plugin.h>
#ifdef __cplusplus
extern "C" {
#endif
/* All calls and callbacks run on the worker dispatcher. The plugin owns its
 * native window, renderer and event loop; no native pointer crosses IPC.
 * One window is registered per worker in v1. Callbacks must return promptly.
 * is_open reports a user close after tick has processed native events. */
#pragma pack(push, 8)
typedef struct et_window_callbacks {
    uint32_t struct_size;
    uint32_t reserved;
    void* context;
    et_result (ET_CALL *open)(void* context);
    et_result (ET_CALL *focus)(void* context);
    et_result (ET_CALL *close)(void* context);
    et_result (ET_CALL *tick)(void* context);
    uint32_t (ET_CALL *is_open)(void* context);
} et_window_callbacks;
typedef struct et_window_service {
    uint32_t struct_size;
    uint32_t version;
    void* context;
    et_result (ET_CALL *register_window)(void* context, const et_window_callbacks* callbacks);
    et_result (ET_CALL *show)(void* context);
    et_result (ET_CALL *focus)(void* context);
    et_result (ET_CALL *close)(void* context);
    uint32_t (ET_CALL *is_open)(void* context);
} et_window_service;
ET_LAYOUT_ASSERT(sizeof(et_window_callbacks) == 56);
ET_LAYOUT_ASSERT(sizeof(et_window_service) == 56);
#pragma pack(pop)
#ifdef __cplusplus
}
#endif
#endif
````

### include/epictuner/sdk/jobs.hpp

````cpp
#pragma once
#include <epictuner/sdk/plugin.hpp>

namespace et::sdk {
class Jobs {
    et_jobs_service service_{};
public:
    et_result connect(const Context& context) noexcept
    { return context.service(ET_SERVICE_JOBS, ET_SERVICE_VERSION_1, &service_, sizeof(service_)); }
    et_result submit(et_job_fn work, void* context, const et_async_options& options, et_handle& handle) const noexcept
    { return service_.submit ? service_.submit(service_.context, work, context, &options, &handle) : ET_ERROR_STATE; }
    et_result cancel(et_handle handle) const noexcept
    { return service_.cancel ? service_.cancel(service_.context, handle) : ET_ERROR_STATE; }
};
}
````

### include/epictuner/sdk/plugin.hpp

````cpp
#pragma once
#include <epictuner/plugin.h>
#include <memory>
#include <algorithm>
#include <string_view>
#include <string>

namespace et::sdk {
inline const char* resultName(et_result code) noexcept {
    switch (code) {
#define ET_RESULT_NAME(name) case name: return #name;
    ET_RESULT_NAME(ET_OK) ET_RESULT_NAME(ET_ERROR_ARGUMENT) ET_RESULT_NAME(ET_ERROR_ABI)
    ET_RESULT_NAME(ET_ERROR_CAPABILITY) ET_RESULT_NAME(ET_ERROR_LIMIT) ET_RESULT_NAME(ET_ERROR_STATE)
    ET_RESULT_NAME(ET_ERROR_CALLBACK) ET_RESULT_NAME(ET_ERROR_UNSUPPORTED) ET_RESULT_NAME(ET_ERROR_STALE)
    ET_RESULT_NAME(ET_ERROR_CANCELLED) ET_RESULT_NAME(ET_ERROR_TIMEOUT) ET_RESULT_NAME(ET_ERROR_PROTOCOL)
    ET_RESULT_NAME(ET_ERROR_CONFLICT) ET_RESULT_NAME(ET_ERROR_NOT_FOUND) ET_RESULT_NAME(ET_ERROR_IO)
    ET_RESULT_NAME(ET_ERROR_BUSY)
#undef ET_RESULT_NAME
    default: return "ET_ERROR_UNKNOWN";
    }
}
// Copy borrowed completion detail while inside the callback. No payload values
// or plugin-owned pointers are retained by this helper.
inline std::string completionMessage(const et_completion& completion, std::string_view step) {
    std::string message(step);
    message += " request=" + std::to_string(completion.operation_id) + " " + resultName(completion.code)
        + " (" + std::to_string(completion.code) + ")";
    if (completion.value.kind == ET_VALUE_U64) message += " revision=" + std::to_string(completion.value.data.u64);
    if (completion.detail.data && completion.detail.size && completion.detail.size <= ET_MAX_ERROR_BYTES)
        message += ": " + std::string(completion.detail.data, completion.detail.size);
    return message;
}
inline et_string string(std::string_view text) noexcept
{
    // Oversize inputs remain invalid instead of silently truncating the length.
    return {text.data(), text.size() > ET_MAX_STRING_BYTES
        ? ET_MAX_STRING_BYTES + 1 : static_cast<uint32_t>(text.size()), 0};
}

class Context {
public:
    explicit Context(const et_host_api* host) noexcept : host_(host) {}
    et_result log(std::string_view text, uint32_t level = ET_LOG_INFO) const noexcept
    { return host_->log(host_->context, level, string(text)); }
    bool has(uint64_t capability) const noexcept
    { return (host_->capabilities & capability) == capability; }
    // Report a named startup step while preserving its real result code.
    et_result check(et_result result, std::string_view step) const {
        if (result != ET_OK) log(std::string(step) + " returned " + resultName(result) + " (" + std::to_string(result) + ")", ET_LOG_ERROR);
        return result;
    }
    et_result check(const et_completion& completion, std::string_view step) const {
        if (completion.code != ET_OK) log(completionMessage(completion, step), ET_LOG_ERROR);
        return completion.code;
    }
    et_result service(uint32_t id, uint32_t version, void* table,
        uint32_t bytes, et_error* error = nullptr) const noexcept
    {
        if (host_->struct_size < sizeof(et_host_api) || !host_->get_service)
            return ET_ERROR_UNSUPPORTED;
        return host_->get_service(host_->context, id, version, table, bytes, error);
    }
private:
    const et_host_api* host_;
};

// Derive optionally to inherit no-op stop/event handlers. All callbacks execute
// serially on the worker dispatcher. No exceptions may escape this adapter.
struct Plugin {
    et_result start(Context&) { return ET_OK; }
    et_result stop() { return ET_OK; }
    et_result onEvent(const et_event&) { return ET_OK; }
};

template<class T> struct Adapter {
    static et_result ET_CALL start(const et_host_api* host, void** instance) noexcept
    {
        if (!instance) return ET_ERROR_ARGUMENT;
        *instance = nullptr;
        if (!host || host->struct_size < ET_HOST_API_V1_SIZE || !host->log)
            return ET_ERROR_ARGUMENT;
        if (ET_ABI_MAJOR(host->abi_version) != ET_ABI_MAJOR(ET_ABI_VERSION)) return ET_ERROR_ABI;
        if ((host->capabilities & T::capabilities) != T::capabilities) return ET_ERROR_CAPABILITY;
        try {
            auto plugin = std::make_unique<T>();
            Context context(host);
            const auto result = plugin->start(context);
            if (result == ET_OK) *instance = plugin.release();
            return result;
        } catch (...) { return ET_ERROR_CALLBACK; }
    }
    static et_result ET_CALL stop(void* instance) noexcept
    {
        if (!instance) return ET_ERROR_ARGUMENT;
        // Deletes in the module that allocated it, even if stop throws.
        std::unique_ptr<T> plugin(static_cast<T*>(instance));
        try { return plugin->stop(); }
        catch (...) { return ET_ERROR_CALLBACK; }
    }
    static et_result ET_CALL event(void* instance, const et_event* event) noexcept
    {
        if (!instance || !event || event->struct_size < sizeof(et_event)
            || event->text.reserved || event->text.size > ET_MAX_STRING_BYTES
            || (!event->text.data && event->text.size)) return ET_ERROR_ARGUMENT;
        try { return static_cast<T*>(instance)->onEvent(*event); }
        catch (...) { return ET_ERROR_CALLBACK; }
    }
    static et_result query(const et_query* query, et_plugin_api* api) noexcept
    {
        if (!query || !api || query->struct_size < sizeof(et_query)
            || api->struct_size < sizeof(et_plugin_api)) return ET_ERROR_ARGUMENT;
        if (ET_ABI_MAJOR(query->abi_version) != ET_ABI_MAJOR(ET_ABI_VERSION)) return ET_ERROR_ABI;
        if ((query->capabilities & T::capabilities) != T::capabilities) return ET_ERROR_CAPABILITY;
        const auto negotiated = std::min(query->abi_version, ET_ABI_VERSION);
        *api = {sizeof(et_plugin_api), negotiated, T::capabilities,
            string(T::id), &start, &stop, &event};
        return ET_OK;
    }
};
} // namespace et::sdk

#define ET_PLUGIN(T) \
    extern "C" ET_EXPORT et_result ET_CALL et_plugin_query( \
        const et_query* query, et_plugin_api* api) { return et::sdk::Adapter<T>::query(query, api); }
````

### include/epictuner/sdk/read.hpp

````cpp
#pragma once
#include <epictuner/read.h>
#include <epictuner/sdk/plugin.hpp>
#include <vector>
#include <string>
namespace et::sdk {
// The caller owns Callback until terminal completion (or service stop).
struct ReadCallback {
    et_read_callback function = nullptr;
    void* owner = nullptr;
    static et_result ET_CALL invoke(void* p, const et_read_reply* r) noexcept {
        try { auto& c = *static_cast<ReadCallback*>(p); return c.function ? c.function(c.owner, r) : ET_OK; }
        catch (...) { return ET_ERROR_CALLBACK; }
    }
};
class Reads {
    et_read_service api_{};
public:
    et_result connect(const Context& c, uint32_t service) { return c.service(service, ET_SERVICE_VERSION_1, &api_, sizeof(api_)); }
    et_result listen(ReadCallback& c) { return api_.listen ? api_.listen(api_.context, ReadCallback::invoke, &c) : ET_ERROR_STATE; }
    et_result request(const et_read_request& r, ReadCallback& c, uint64_t* operation = nullptr) {
        uint64_t ignored{}; return api_.request ? api_.request(api_.context, &r, ReadCallback::invoke, &c, operation ? operation : &ignored) : ET_ERROR_STATE;
    }
    et_result cancel(uint64_t operation) { return api_.cancel ? api_.cancel(api_.context, operation) : ET_ERROR_STATE; }
    static et_read_request make(uint32_t op, uint64_t generation = 0) {
        et_read_request r{}; r.struct_size = sizeof(r); r.operation = op; r.project_generation = generation; r.count = ET_READ_MAX_ROWS; return r;
    }
};
}
````

### include/epictuner/sdk/tune.hpp

````cpp
#pragma once
#include <epictuner/tune.h>
#include <epictuner/sdk/read.hpp>
#include <charconv>
#include <cmath>
#include <stdexcept>
namespace et::sdk {
struct TuneChange { std::string field; uint32_t index=0; double expected=0, value=0; };
class Tune : public Reads {
public:
    et_result connect(const Context& c) { return Reads::connect(c,ET_SERVICE_TUNE); }
    static std::string proposal(std::string_view label,const std::vector<TuneChange>& changes) {
        if(label.empty()||label.size()>128||label.find('\0')!=std::string_view::npos||label.find_first_of("\n\r\t")!=std::string_view::npos||changes.empty()||changes.size()>ET_TUNE_MAX_EDITS)throw std::invalid_argument("Invalid proposal");
        auto number=[](double value){if(!std::isfinite(value))throw std::invalid_argument("Non-finite change");char b[64];auto result=std::to_chars(b,b+sizeof(b),value,std::chars_format::general,17);if(result.ec!=std::errc{})throw std::invalid_argument("Invalid number");return std::string(b,result.ptr);};
        std::string out(label);
        for(const auto& e:changes){if(e.field.empty()||e.field.find('\0')!=std::string::npos||e.field.find_first_of("\n\t\r")!=std::string::npos)throw std::invalid_argument("Invalid field");out+='\n'+e.field+'\t'+std::to_string(e.index)+'\t'+number(e.expected)+'\t'+number(e.value);}
        if(out.size()>ET_MAX_STRING_BYTES)throw std::length_error("Proposal exceeds string limit");return out;
    }
};
}
````

### include/epictuner/sdk/ui.hpp

````cpp
#pragma once
#include <epictuner/ui.h>
#include <epictuner/sdk/plugin.hpp>
#include <deque>
#include <string>
#include <vector>

namespace et::sdk {
// Owns construction storage. No Qt, JSON, callbacks or C++ objects cross the ABI.
class PanelBuilder {
    struct Node { std::string id, parent; uint32_t type; std::vector<et_ui_property> properties; };
    std::deque<Node> nodes_;
    std::deque<std::string> strings_;
    std::vector<et_ui_node> wire_;
public:
    et_ui_panel definition{};
    PanelBuilder(std::string_view id, std::string_view title) {
        definition.struct_size = sizeof(definition);
        definition.id = keep(id); definition.title = keep(title);
        definition.menu = keep(title); definition.placements = ET_PLACEMENT_WORKSPACE | ET_PLACEMENT_FLOATING;
        definition.flags = ET_PANEL_PERSIST; definition.state_schema = 1;
        definition.width = 720; definition.height = 540; definition.min_width = 360; definition.min_height = 240;
    }
    PanelBuilder(const PanelBuilder&) = delete;
    PanelBuilder& operator=(const PanelBuilder&) = delete;
    et_string keep(std::string_view text) { strings_.emplace_back(text); return string(strings_.back()); }
    PanelBuilder& add(uint32_t type, std::string_view id, std::string_view parent = {}) {
        nodes_.push_back({std::string(id), std::string(parent), type, {}}); return *this;
    }
    PanelBuilder& prefab(uint32_t type, std::string_view id, std::string_view target = {}, std::string_view parent = {}) {
        return add(type,id,parent).text(ET_UI_TARGET,target);
    }
    PanelBuilder& text(uint32_t property, std::string_view value) {
        et_value v{}; v.kind = ET_VALUE_STRING; v.data.string = keep(value); return set(property, v);
    }
    PanelBuilder& number(uint32_t property, double value) {
        et_value v{}; v.kind = ET_VALUE_F64; v.data.f64 = value; return set(property, v);
    }
    PanelBuilder& boolean(uint32_t property, bool value) {
        et_value v{}; v.kind = ET_VALUE_BOOL; v.data.boolean = value; return set(property, v);
    }
    PanelBuilder& selectedIndex(int index) { return number(ET_UI_SELECTED_INDEX, index); }
    PanelBuilder& set(uint32_t property, et_value value) {
        nodes_.at(nodes_.size() - 1).properties.push_back({property, 0, value}); return *this;
    }
    const et_ui_panel* build() {
        wire_.clear();
        for (const auto& n : nodes_) wire_.push_back({sizeof(et_ui_node), n.type, string(n.id), string(n.parent),
            n.properties.data(), static_cast<uint32_t>(n.properties.size()), 0});
        definition.nodes = wire_.data(); definition.node_count = static_cast<uint32_t>(wire_.size());
        return &definition;
    }
};
class Ui {
    et_ui_service api_{};
    et_ui_event_fn callback_ = nullptr;
    void* owner_ = nullptr;
    static et_result ET_CALL dispatch(void* context, const et_ui_event* event) noexcept {
        auto* self = static_cast<Ui*>(context);
        try { return self->callback_ ? self->callback_(self->owner_, event) : ET_OK; }
        catch (...) { return ET_ERROR_CALLBACK; }
    }
public:
    Ui() = default;
    Ui(const Ui&) = delete;
    Ui& operator=(const Ui&) = delete;
    et_result connect(const Context& context, et_ui_event_fn callback, void* owner) {
        const auto result = context.service(ET_SERVICE_UI, ET_SERVICE_VERSION_1, &api_, sizeof(api_));
        callback_ = callback; owner_ = owner;
        return result == ET_OK ? api_.listen(api_.context, dispatch, this) : result;
    }
    et_result define(PanelBuilder& builder, const et_async_options* completion = nullptr) const {
        return api_.define_panel ? api_.define_panel(api_.context, builder.build(), completion) : ET_ERROR_STATE;
    }
    et_result open(std::string_view panel, std::string_view instance = {}, uint32_t placement = 0,
        const et_async_options* completion = nullptr) const {
        return api_.open ? api_.open(api_.context, string(panel), string(instance), placement, completion) : ET_ERROR_STATE;
    }
    et_result patch(et_handle panel, const std::vector<et_ui_patch>& changes, uint32_t revision,
        uint32_t origin = 0, const et_async_options* completion = nullptr) const {
        return api_.patch ? api_.patch(api_.context, panel, changes.data(), static_cast<uint32_t>(changes.size()), revision, origin, completion) : ET_ERROR_STATE;
    }
    et_result action(et_handle panel, uint32_t action, uint32_t origin = 0,
        const et_async_options* completion = nullptr) const {
        return api_.action ? api_.action(api_.context, panel, action, origin, completion) : ET_ERROR_STATE;
    }
    static et_ui_patch text(std::string_view node, uint32_t property, std::string_view value) {
        et_value v{}; v.kind = ET_VALUE_STRING; v.data.string = string(value);
        return {string(node), {property, 0, v}};
    }
    static et_ui_patch number(std::string_view node, uint32_t property, double value) {
        et_value v{}; v.kind = ET_VALUE_F64; v.data.f64 = value;
        return {string(node), {property, 0, v}};
    }
    static et_ui_patch selectedIndex(std::string_view node, int index) {
        return number(node, ET_UI_SELECTED_INDEX, index);
    }
};
}
````

### include/epictuner/sdk/values.hpp

````cpp
#pragma once
#include <epictuner/plugin.h>
#include <cmath>
#include <string_view>

namespace et::sdk {
// Validation never dereferences a handle. Strings/bytes are caller-owned spans;
// the pointer must reference the advertised readable storage during the call.
inline bool validUtf8(std::string_view text) noexcept
{
    for (size_t i = 0; i < text.size();) {
        const auto first = static_cast<unsigned char>(text[i++]);
        if (!first) return false;
        if (first < 0x80) continue;
        int extra; uint32_t scalar, minimum;
        if (first >= 0xc2 && first <= 0xdf) { extra = 1; scalar = first & 31; minimum = 0x80; }
        else if (first >= 0xe0 && first <= 0xef) { extra = 2; scalar = first & 15; minimum = 0x800; }
        else if (first >= 0xf0 && first <= 0xf4) { extra = 3; scalar = first & 7; minimum = 0x10000; }
        else return false;
        if (i + extra > text.size()) return false;
        for (int n = 0; n < extra; ++n) {
            const auto next = static_cast<unsigned char>(text[i++]);
            if ((next & 0xc0) != 0x80) return false;
            scalar = (scalar << 6) | (next & 63);
        }
        if (scalar < minimum || scalar > 0x10ffff || (scalar >= 0xd800 && scalar <= 0xdfff)) return false;
    }
    return true;
}
inline bool valid(et_string text) noexcept
{
    return !text.reserved && text.size <= ET_MAX_STRING_BYTES
        && (text.data || !text.size) && (!text.size || validUtf8({text.data, text.size}));
}
inline bool valid(et_handle handle) noexcept
{
    return handle.session && handle.slot && handle.generation && !handle.reserved
        && handle.kind >= ET_HANDLE_PANEL && handle.kind <= ET_HANDLE_PROPOSAL;
}
inline bool valid(const et_value& value) noexcept
{
    if (value.reserved) return false;
    switch (value.kind) {
    case ET_VALUE_NULL: return true;
    case ET_VALUE_BOOL: return value.data.boolean <= 1;
    case ET_VALUE_I64: case ET_VALUE_U64: return true;
    case ET_VALUE_F64: return std::isfinite(value.data.f64);
    case ET_VALUE_STRING: return valid(value.data.string);
    case ET_VALUE_BYTES: return !value.data.bytes.reserved && value.data.bytes.size <= ET_MAX_FRAME_BYTES
        && (value.data.bytes.data || !value.data.bytes.size);
    case ET_VALUE_HANDLE: return valid(value.data.handle);
    default: return false;
    }
}
}
````

### include/epictuner/sdk/window.hpp

````cpp
#pragma once
#include <epictuner/window.h>
#include <epictuner/sdk/plugin.hpp>

namespace et::sdk {
class Window {
public:
    et_result connect(const Context& host, const et_window_callbacks& callbacks) noexcept {
        if (!host.has(ET_CAP_CUSTOM_WINDOW)) return ET_ERROR_CAPABILITY;
        auto result=host.service(ET_SERVICE_CUSTOM_WINDOW,ET_SERVICE_VERSION_1,&api_,sizeof(api_));
        return result==ET_OK ? api_.register_window(api_.context,&callbacks) : result;
    }
    et_result show() const noexcept { return api_.context ? api_.show(api_.context) : ET_ERROR_STATE; }
    et_result focus() const noexcept { return api_.context ? api_.focus(api_.context) : ET_ERROR_STATE; }
    et_result close() const noexcept { return api_.context ? api_.close(api_.context) : ET_ERROR_STATE; }
    bool isOpen() const noexcept { return api_.context && api_.is_open(api_.context); }
private:
    et_window_service api_{};
};
}
````

### schemas/development-manifest.schema.json

````json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "EpicTuner development manifest v1",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema",
    "id",
    "name",
    "version",
    "abi",
    "platform",
    "entry",
    "requiredFeatures",
    "optionalFeatures",
    "capabilities"
  ],
  "properties": {
    "schema": {
      "const": 1
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "\\S"
    },
    "version": {
      "type": "string",
      "minLength": 1,
      "maxLength": 32,
      "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
    },
    "abi": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "major",
        "minMinor"
      ],
      "properties": {
        "major": {
          "const": 1
        },
        "minMinor": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1
        }
      }
    },
    "platform": {
      "enum": [
        "windows-x64",
        "linux-x64",
        "linux-arm64"
      ]
    },
    "entry": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*\\.(dll|so)$"
    },
    "requiredFeatures": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "enum": [
          "et.context.v1",
          "et.log.v1",
          "et.events.v1",
          "et.ui.v1",
          "et.project.v1",
          "et.channels.v1",
          "et.datalog.v1",
          "et.settings.v1",
          "et.jobs.v1",
          "et.tune.v1",
          "et.prefabs.v1",
          "et.surface.v1"
        ]
      }
    },
    "optionalFeatures": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128
      }
    },
    "capabilities": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "enum": [
          "log",
          "events",
          "ui",
          "project.read",
          "channels.read",
          "datalog.read",
          "settings",
          "jobs",
          "tune.read",
          "tune.propose",
          "tune.apply",
          "tune.burn"
        ]
      }
    }
  }
}
````

### schemas/envelope.schema.json

````json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "EpicTuner worker wire v1",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "wire",
    "session",
    "seq",
    "request",
    "generation",
    "type",
    "body"
  ],
  "properties": {
    "wire": {
      "const": 1
    },
    "session": {
      "type": "string",
      "format": "uuid"
    },
    "seq": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4294967295
    },
    "request": {
      "type": "integer",
      "minimum": 0,
      "maximum": 4294967295
    },
    "generation": {
      "const": 0
    },
    "type": {
      "enum": [
        "hello",
        "welcome",
        "ready",
        "log",
        "ping",
        "pong",
        "stop",
        "stopped",
        "event",
        "result",
        "error",
        "ui",
        "ui_result",
        "ui_event",
        "read",
        "read_result",
        "read_event",
        "read_cancel",
        "read_ack"
      ]
    },
    "body": {
      "type": "object"
    }
  },
  "allOf": [
    {
      "if": {
        "properties": {
          "type": {
            "const": "hello"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "token",
              "id"
            ],
            "properties": {
              "token": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64,
                "pattern": "^[a-f0-9]{64}$"
              },
              "id": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128,
                "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
              }
            }
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "welcome"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "capabilities"
            ],
            "properties": {
              "capabilities": {
                "type": "integer",
                "minimum": 0,
                "maximum": 255
              }
            }
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "ready"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "log"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "level",
              "text"
            ],
            "properties": {
              "level": {
                "type": "integer",
                "minimum": 1,
                "maximum": 3
              },
              "text": {
                "type": "string",
                "maxLength": 4096
              }
            }
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "ping"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "pong"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "stop"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "stopped"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "event"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type",
              "text"
            ],
            "properties": {
              "type": {
                "const": 1
              },
              "text": {
                "type": "string",
                "maxLength": 4096
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "result"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "minimum": 0,
                "maximum": 15
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "error"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "detail"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "minimum": 1,
                "maximum": 15
              },
              "detail": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            }
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "ui"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "op",
                  "panel"
                ],
                "properties": {
                  "op": {
                    "const": "define"
                  },
                  "panel": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "id",
                      "title",
                      "menu",
                      "flags",
                      "placements",
                      "schema",
                      "width",
                      "height",
                      "minWidth",
                      "minHeight",
                      "nodes"
                    ],
                    "properties": {
                      "id": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 128,
                        "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"
                      },
                      "title": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 4096
                      },
                      "menu": {
                        "type": "string",
                        "maxLength": 4096
                      },
                      "flags": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 15
                      },
                      "placements": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 7
                      },
                      "schema": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 65535
                      },
                      "width": {
                        "type": "integer",
                        "minimum": 100,
                        "maximum": 4096
                      },
                      "height": {
                        "type": "integer",
                        "minimum": 100,
                        "maximum": 4096
                      },
                      "minWidth": {
                        "type": "integer",
                        "minimum": 100,
                        "maximum": 4096
                      },
                      "minHeight": {
                        "type": "integer",
                        "minimum": 100,
                        "maximum": 4096
                      },
                      "nodes": {
                        "type": "array",
                        "maxItems": 4096,
                        "minItems": 1,
                        "uniqueItems": true,
                        "items": {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "id",
                            "parent",
                            "type",
                            "props"
                          ],
                          "properties": {
                            "id": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 128,
                              "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"
                            },
                            "parent": {
                              "type": "string",
                              "maxLength": 128
                            },
                            "type": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 32
                            },
                            "props": {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [],
                              "properties": {
                                "title": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "units": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "options": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "tooltip": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "error": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "bindValue": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "bindEnabled": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "bindVisible": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "data": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "source": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "help": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "target": {
                                  "type": "string",
                                  "maxLength": 4096
                                },
                                "enabled": {
                                  "type": "boolean"
                                },
                                "visible": {
                                  "type": "boolean"
                                },
                                "twoWay": {
                                  "type": "boolean"
                                },
                                "persist": {
                                  "type": "boolean"
                                },
                                "fill": {
                                  "type": "boolean"
                                },
                                "readOnly": {
                                  "type": "boolean"
                                },
                                "minimum": {
                                  "type": "number"
                                },
                                "maximum": {
                                  "type": "number"
                                },
                                "step": {
                                  "type": "number"
                                },
                                "decimals": {
                                  "type": "number"
                                },
                                "minWidth": {
                                  "type": "number"
                                },
                                "preferredWidth": {
                                  "type": "number"
                                },
                                "maxWidth": {
                                  "type": "number"
                                },
                                "columns": {
                                  "type": "number"
                                },
                                "minHeight": {
                                  "type": "number"
                                },
                                "preferredHeight": {
                                  "type": "number"
                                },
                                "maxHeight": {
                                  "type": "number"
                                },
                                "stretch": {
                                  "type": "number"
                                },
                                "value": {
                                  "type": [
                                    "string",
                                    "number",
                                    "boolean",
                                    "null"
                                  ],
                                  "maxLength": 4096
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "op",
                  "definition",
                  "instance",
                  "placement"
                ],
                "properties": {
                  "op": {
                    "const": "open"
                  },
                  "definition": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128,
                    "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"
                  },
                  "instance": {
                    "type": "string",
                    "maxLength": 128
                  },
                  "placement": {
                    "enum": [
                      0,
                      1,
                      2,
                      4
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "op",
                  "handle",
                  "patches",
                  "revision",
                  "origin"
                ],
                "properties": {
                  "op": {
                    "const": "patch"
                  },
                  "handle": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "const": 1
                      }
                    }
                  },
                  "patches": {
                    "type": "array",
                    "maxItems": 4096,
                    "minItems": 1,
                    "uniqueItems": true,
                    "items": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "node",
                        "props"
                      ],
                      "properties": {
                        "node": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 128,
                          "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"
                        },
                        "props": {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [],
                          "properties": {
                            "title": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "units": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "options": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "tooltip": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "error": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "bindValue": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "bindEnabled": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "bindVisible": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "data": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "source": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "help": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "target": {
                              "type": "string",
                              "maxLength": 4096
                            },
                            "enabled": {
                              "type": "boolean"
                            },
                            "visible": {
                              "type": "boolean"
                            },
                            "twoWay": {
                              "type": "boolean"
                            },
                            "persist": {
                              "type": "boolean"
                            },
                            "fill": {
                              "type": "boolean"
                            },
                            "readOnly": {
                              "type": "boolean"
                            },
                            "minimum": {
                              "type": "number"
                            },
                            "maximum": {
                              "type": "number"
                            },
                            "step": {
                              "type": "number"
                            },
                            "decimals": {
                              "type": "number"
                            },
                            "minWidth": {
                              "type": "number"
                            },
                            "preferredWidth": {
                              "type": "number"
                            },
                            "maxWidth": {
                              "type": "number"
                            },
                            "columns": {
                              "type": "number"
                            },
                            "minHeight": {
                              "type": "number"
                            },
                            "preferredHeight": {
                              "type": "number"
                            },
                            "maxHeight": {
                              "type": "number"
                            },
                            "stretch": {
                              "type": "number"
                            },
                            "value": {
                              "type": [
                                "string",
                                "number",
                                "boolean",
                                "null"
                              ],
                              "maxLength": 4096
                            }
                          }
                        }
                      }
                    }
                  },
                  "revision": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 4294967295
                  },
                  "origin": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 4294967295
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "op",
                  "handle",
                  "action",
                  "origin"
                ],
                "properties": {
                  "op": {
                    "const": "action"
                  },
                  "handle": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "const": 1
                      }
                    }
                  },
                  "action": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 4
                  },
                  "origin": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 4294967295
                  }
                }
              }
            ]
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "ui_result"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "detail",
              "handle",
              "revision"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "minimum": 0,
                "maximum": 15
              },
              "detail": {
                "type": "string",
                "maxLength": 512
              },
              "handle": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "const": 1
                      }
                    }
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [],
                    "properties": {}
                  }
                ]
              },
              "revision": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "ui_event"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type",
              "handle",
              "definition",
              "instance",
              "node",
              "value",
              "revision",
              "origin"
            ],
            "properties": {
              "type": {
                "type": "integer",
                "minimum": 1,
                "maximum": 9
              },
              "handle": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "session",
                  "project",
                  "slot",
                  "generation",
                  "kind"
                ],
                "properties": {
                  "session": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20,
                    "pattern": "^[1-9][0-9]*$"
                  },
                  "project": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20,
                    "pattern": "^(0|[1-9][0-9]*)$"
                  },
                  "slot": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 4294967295
                  },
                  "generation": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 4294967295
                  },
                  "kind": {
                    "const": 1
                  }
                }
              },
              "definition": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128,
                "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,127}$"
              },
              "instance": {
                "type": "string",
                "maxLength": 128
              },
              "node": {
                "type": "string",
                "maxLength": 128
              },
              "value": {
                "type": [
                  "string",
                  "number",
                  "boolean",
                  "null"
                ],
                "maxLength": 4096
              },
              "revision": {
                "type": "integer",
                "minimum": 1,
                "maximum": 4294967295
              },
              "origin": {
                "type": "integer",
                "minimum": 1,
                "maximum": 4294967295
              }
            }
          },
          "request": {
            "const": 0
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "read"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "op",
              "generation",
              "handle",
              "offset",
              "count",
              "flags",
              "schema",
              "id",
              "text",
              "channels",
              "value",
              "timestamp"
            ],
            "properties": {
              "op": {
                "enum": [
                  1,
                  2,
                  3,
                  4,
                  5,
                  6,
                  7,
                  8,
                  9,
                  10,
                  11,
                  12,
                  13,
                  20,
                  21,
                  22,
                  23,
                  24,
                  25,
                  26,
                  27,
                  28
                ]
              },
              "generation": {
                "type": "string",
                "minLength": 1,
                "maxLength": 20,
                "pattern": "^(0|[1-9][0-9]*)$"
              },
              "handle": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "enum": [
                          4,
                          7
                        ]
                      }
                    }
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [],
                    "properties": {}
                  }
                ]
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "count": {
                "type": "integer",
                "minimum": 1,
                "maximum": 32
              },
              "flags": {
                "type": "integer",
                "minimum": 0,
                "maximum": 3
              },
              "schema": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "id": {
                "type": "string",
                "maxLength": 4096
              },
              "text": {
                "type": "string",
                "maxLength": 4096
              },
              "channels": {
                "type": "array",
                "maxItems": 16,
                "minItems": 0,
                "uniqueItems": true,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "value": {
                "type": "number"
              },
              "timestamp": {
                "type": "number"
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "read_result"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "generation",
              "handle",
              "total",
              "offset",
              "gaps",
              "schema",
              "flags",
              "event",
              "text",
              "items"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "minimum": 0,
                "maximum": 15
              },
              "generation": {
                "type": "string",
                "minLength": 1,
                "maxLength": 20,
                "pattern": "^(0|[1-9][0-9]*)$"
              },
              "handle": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "enum": [
                          4,
                          7
                        ]
                      }
                    }
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [],
                    "properties": {}
                  }
                ]
              },
              "total": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "gaps": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "schema": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "flags": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4095
              },
              "event": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2
              },
              "text": {
                "type": "string",
                "maxLength": 4096
              },
              "items": {
                "type": "array",
                "maxItems": 512,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "label",
                    "units",
                    "value",
                    "timestamp",
                    "sequence",
                    "flags"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "label": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "units": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "value": {
                      "type": "number"
                    },
                    "timestamp": {
                      "type": "number"
                    },
                    "sequence": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 20,
                      "pattern": "^(0|[1-9][0-9]*)$"
                    },
                    "flags": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 4095
                    }
                  }
                }
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "read_event"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "generation",
              "handle",
              "total",
              "offset",
              "gaps",
              "schema",
              "flags",
              "event",
              "text",
              "items"
            ],
            "properties": {
              "code": {
                "type": "integer",
                "minimum": 0,
                "maximum": 15
              },
              "generation": {
                "type": "string",
                "minLength": 1,
                "maxLength": 20,
                "pattern": "^(0|[1-9][0-9]*)$"
              },
              "handle": {
                "oneOf": [
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "session",
                      "project",
                      "slot",
                      "generation",
                      "kind"
                    ],
                    "properties": {
                      "session": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^[1-9][0-9]*$"
                      },
                      "project": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 20,
                        "pattern": "^(0|[1-9][0-9]*)$"
                      },
                      "slot": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "generation": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 4294967295
                      },
                      "kind": {
                        "enum": [
                          4,
                          7
                        ]
                      }
                    }
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [],
                    "properties": {}
                  }
                ]
              },
              "total": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "gaps": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "schema": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4294967295
              },
              "flags": {
                "type": "integer",
                "minimum": 0,
                "maximum": 4095
              },
              "event": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2
              },
              "text": {
                "type": "string",
                "maxLength": 4096
              },
              "items": {
                "type": "array",
                "maxItems": 512,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "label",
                    "units",
                    "value",
                    "timestamp",
                    "sequence",
                    "flags"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "label": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "units": {
                      "type": "string",
                      "maxLength": 4096
                    },
                    "value": {
                      "type": "number"
                    },
                    "timestamp": {
                      "type": "number"
                    },
                    "sequence": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 20,
                      "pattern": "^(0|[1-9][0-9]*)$"
                    },
                    "flags": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 4095
                    }
                  }
                }
              }
            }
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "read_cancel"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "read_ack"
          }
        }
      },
      "then": {
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": false,
            "required": [],
            "properties": {}
          },
          "request": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4294967295
          }
        }
      }
    }
  ]
}
````

### schemas/package-manifest.schema.json

````json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "EpicTuner .etplugin package manifest v1",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema",
    "id",
    "name",
    "description",
    "version",
    "publisher",
    "urls",
    "host",
    "abi",
    "requiredFeatures",
    "optionalFeatures",
    "capabilities",
    "targets",
    "panels",
    "services",
    "settingsSchema",
    "files",
    "dependencies"
  ],
  "properties": {
    "schema": {
      "const": 1
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "\\S"
    },
    "description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 4096
    },
    "version": {
      "type": "string",
      "minLength": 1,
      "maxLength": 32,
      "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
    },
    "publisher": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "name",
        "keyId"
      ],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "keyId": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
        }
      }
    },
    "urls": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "home",
        "help",
        "support"
      ],
      "properties": {
        "home": {
          "type": "string",
          "minLength": 1,
          "maxLength": 2048,
          "format": "uri",
          "pattern": "^https://"
        },
        "help": {
          "type": "string",
          "minLength": 1,
          "maxLength": 2048,
          "format": "uri",
          "pattern": "^https://"
        },
        "support": {
          "type": "string",
          "minLength": 1,
          "maxLength": 2048,
          "format": "uri",
          "pattern": "^https://"
        }
      }
    },
    "host": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "minimum",
        "maximumExclusive"
      ],
      "properties": {
        "minimum": {
          "type": "string",
          "minLength": 1,
          "maxLength": 32,
          "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
        },
        "maximumExclusive": {
          "type": "string",
          "minLength": 1,
          "maxLength": 32,
          "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
        }
      }
    },
    "abi": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "major",
        "minMinor"
      ],
      "properties": {
        "major": {
          "const": 1
        },
        "minMinor": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1
        }
      }
    },
    "requiredFeatures": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "enum": [
          "et.context.v1",
          "et.log.v1",
          "et.events.v1",
          "et.ui.v1",
          "et.project.v1",
          "et.channels.v1",
          "et.datalog.v1",
          "et.settings.v1",
          "et.jobs.v1",
          "et.tune.v1",
          "et.prefabs.v1",
          "et.surface.v1",
          "et.files.v1",
          "et.license.v1",
          "et.custom-window.v1"
        ]
      }
    },
    "optionalFeatures": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128
      }
    },
    "capabilities": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "enum": [
          "log",
          "events",
          "ui",
          "project.read",
          "channels.read",
          "datalog.read",
          "settings",
          "jobs",
          "tune.read",
          "tune.propose",
          "tune.apply",
          "tune.burn",
          "files",
          "license",
          "custom-window"
        ]
      }
    },
    "targets": {
      "type": "array",
      "maxItems": 3,
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "platform",
          "entry"
        ],
        "properties": {
          "platform": {
            "enum": [
              "windows-x64",
              "linux-x64",
              "linux-arm64"
            ]
          },
          "entry": {
            "type": "string",
            "minLength": 1,
            "maxLength": 240,
            "pattern": "^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_.-]+)*$"
          }
        }
      }
    },
    "panels": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "title",
          "placements",
          "multipleInstances",
          "projectRequired",
          "stateSchema"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "placements": {
            "type": "array",
            "maxItems": 3,
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "enum": [
                "workspace",
                "floating",
                "modal"
              ]
            }
          },
          "multipleInstances": {
            "type": "boolean"
          },
          "projectRequired": {
            "type": "boolean"
          },
          "stateSchema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          }
        }
      }
    },
    "services": {
      "type": "array",
      "maxItems": 32,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128,
        "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
      }
    },
    "settingsSchema": {
      "type": "integer",
      "minimum": 0,
      "maximum": 65535
    },
    "files": {
      "type": "array",
      "maxItems": 4096,
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "path",
          "bytes",
          "sha256",
          "role",
          "platform"
        ],
        "properties": {
          "path": {
            "type": "string",
            "minLength": 1,
            "maxLength": 240,
            "pattern": "^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_.-]+)*$"
          },
          "bytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 67108864
          },
          "sha256": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "pattern": "^[a-f0-9]{64}$"
          },
          "role": {
            "enum": [
              "entry",
              "dependency",
              "asset",
              "license"
            ]
          },
          "platform": {
            "enum": [
              "windows-x64",
              "linux-x64",
              "linux-arm64",
              "any"
            ]
          }
        }
      }
    },
    "dependencies": {
      "type": "array",
      "maxItems": 128,
      "minItems": 0,
      "uniqueItems": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "version",
          "license",
          "path",
          "platform"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "version": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "license": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "path": {
            "type": "string",
            "minLength": 1,
            "maxLength": 240,
            "pattern": "^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_.-]+)*$"
          },
          "platform": {
            "enum": [
              "windows-x64",
              "linux-x64",
              "linux-arm64"
            ]
          }
        }
      }
    },
    "product": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "policy",
        "entitlementFeatures"
      ],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128,
          "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
        },
        "policy": {
          "enum": [
            "free",
            "trial",
            "perpetual",
            "subscription"
          ]
        },
        "entitlementFeatures": {
          "type": "array",
          "maxItems": 32,
          "minItems": 0,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
          }
        }
      }
    }
  }
}
````

### schemas/signature.schema.json

````json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "EpicTuner detached package signature v1",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema",
    "algorithm",
    "keyId",
    "manifestSha256",
    "signature"
  ],
  "properties": {
    "schema": {
      "const": 1
    },
    "algorithm": {
      "const": "Ed25519"
    },
    "keyId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "pattern": "^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$"
    },
    "manifestSha256": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "pattern": "^[a-f0-9]{64}$"
    },
    "signature": {
      "type": "string",
      "minLength": 1,
      "maxLength": 86,
      "pattern": "^[A-Za-z0-9_-]{86}$"
    }
  }
}
````

### templates/panel/CMakeLists.txt

````cmake
cmake_minimum_required(VERSION 3.24)
project(EpicTunerStarter LANGUAGES CXX)
find_package(EpicTunerSDK 1 REQUIRED CONFIG)
include(${EpicTunerSDK_DIR}/EpicTunerPlugin.cmake)
add_library(starter MODULE plugin.cpp)
target_link_libraries(starter PRIVATE EpicTuner::SDKCpp)
epictuner_development_manifest(starter)
````

### templates/panel/plugin.cpp

````cpp
#include <epictuner/sdk/ui.hpp>
#include <optional>

using namespace et::sdk;

// The worker owns this object. Keep callback owners alive until stop completes.
class Starter : public Plugin {
    Ui ui;
    std::optional<Context> context;
    et_async_options completion{sizeof(et_async_options), ET_OPERATION_TIMEOUT_MS,
                               0, 0, this, completed};
    static void ET_CALL completed(void* owner, const et_completion* result) noexcept {
        auto& self = *static_cast<Starter*>(owner);
        try { self.context->check(*result, "Starter panel operation"); } catch (...) {}
    }
    static et_result ET_CALL input(void* owner, const et_ui_event* event) {
        auto& self = *static_cast<Starter*>(owner);
        const std::string_view node(event->node.data ? event->node.data : "", event->node.size);
        if (event->type == ET_UI_CLICK && node == "hello")
            return self.ui.patch(event->panel,
                {Ui::text("status", ET_UI_TITLE, "Hello from your compiled plugin!")},
                event->revision, event->origin, &self.completion);
        return ET_OK;
    }
public:
    static constexpr std::string_view id = "org.example.starter";
    static constexpr uint64_t capabilities = ET_CAP_UI | ET_CAP_LOG | ET_CAP_EVENTS;
    et_result start(Context& host) {
        context = host;
        auto result = host.check(ui.connect(host, input, this), "ui.connect");
        if (result != ET_OK) return result;
        PanelBuilder panel("org.example.starter.main", "Starter panel");
        panel.definition.menu = panel.keep("Examples/Starter panel");
        panel.add(ET_UI_COLUMN, "root");
        panel.add(ET_UI_TEXT, "name", "root").text(ET_UI_TITLE, "Name")
            .text(ET_UI_VALUE, "EpicTuner").boolean(ET_UI_PERSIST, true);
        panel.add(ET_UI_BUTTON, "hello", "root").text(ET_UI_TITLE, "Say hello");
        panel.add(ET_UI_LABEL, "status", "root").text(ET_UI_TITLE, "Ready");
        return host.check(ui.define(panel, &completion), "ui.define starter panel");
    }
};
ET_PLUGIN(Starter)
````

### templates/panel/plugin.json.in

````json
{
  "schema": 1,
  "id": "org.example.starter",
  "name": "Starter panel",
  "version": "1.0.0",
  "abi": {"major": 1, "minMinor": 1},
  "platform": "@ET_PLATFORM@",
  "entry": "starter@CMAKE_SHARED_MODULE_SUFFIX@",
  "requiredFeatures": ["et.ui.v1", "et.events.v1", "et.log.v1"],
  "optionalFeatures": [],
  "capabilities": ["ui", "events", "log"]
}
````

### examples/cascading-selects/CMakeLists.txt

````cmake
cmake_minimum_required(VERSION 3.24)
project(EpicTunerCascadingSelects LANGUAGES CXX)
find_package(EpicTunerSDK 1 REQUIRED CONFIG)
include(${EpicTunerSDK_DIR}/EpicTunerPlugin.cmake)
add_library(cascading-selects MODULE plugin.cpp)
target_link_libraries(cascading-selects PRIVATE EpicTuner::SDKCpp)
epictuner_development_manifest(cascading-selects)
````

### examples/cascading-selects/plugin.cpp

````cpp
#include <epictuner/sdk/ui.hpp>
#include <optional>

using namespace et::sdk;
class CascadingSelects : public Plugin {
    Ui ui;
    std::optional<Context> context;
    et_async_options completion{sizeof(et_async_options), ET_OPERATION_TIMEOUT_MS, 0, 0, this, completed};
    static void ET_CALL completed(void* owner, const et_completion* result) noexcept {
        if (result->code == ET_OK) return;
        auto& self = *static_cast<CascadingSelects*>(owner);
        try { self.context->check(*result, "Cascading selections"); } catch (...) {}
    }
    static et_result ET_CALL input(void* owner, const et_ui_event* e) {
        auto& self = *static_cast<CascadingSelects*>(owner);
        if (e->type != ET_UI_EVENT_SELECTED || e->value.kind != ET_VALUE_F64) return ET_OK;
        const std::string_view node(e->node.data, e->node.size);
        const int selected = static_cast<int>(e->value.data.f64);
        // Separate calls in one event are applied in order by host 0.8.3+.
        const auto status = self.ui.patch(e->panel, {Ui::text("notice", ET_UI_TITLE, "Updating selections and preview...")}, e->revision, e->origin, &self.completion);
        if (status != ET_OK) return status;
        std::vector<et_ui_patch> changes;
        if (node == "make") {
            if (selected == 12) {
                // Programmatic selection does not fire a user input event. Set
                // the dependent selections and preview explicitly.
                changes = {Ui::text("model", ET_UI_OPTIONS, "Only model"), Ui::selectedIndex("model", 0),
                    Ui::text("tune", ET_UI_OPTIONS, "Only tune"), Ui::selectedIndex("tune", 0),
                    Ui::text("preview", ET_UI_VALUE, "8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\n2.0\t1.8\t1.6\t1.4\t1.2\t1.0\t0.8\t0.7\t0.6\t0.5\t0.4\t0.3")};
            } else {
            changes = {Ui::text("model", ET_UI_OPTIONS, selected % 2 ? "Model C\nModel D" : "Model A\nModel B"),
                Ui::selectedIndex("model", -1), Ui::text("tune", ET_UI_OPTIONS, ""), Ui::selectedIndex("tune", -1),
                Ui::text("preview", ET_UI_VALUE, "")};
            }
        } else if (node == "model") {
            changes = {Ui::text("tune", ET_UI_OPTIONS, selected ? "Tune C\nTune D" : "Tune A\nTune B"),
                Ui::selectedIndex("tune", -1), Ui::text("preview", ET_UI_VALUE, "")};
        } else if (node == "tune") {
            // Two rows, twelve columns. Synthetic values only; no ECU writes.
            changes = {Ui::text("preview", ET_UI_VALUE, selected
                ? "8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\n2.1\t1.9\t1.7\t1.5\t1.3\t1.1\t0.9\t0.8\t0.7\t0.6\t0.5\t0.4"
                : "8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\n2.0\t1.8\t1.6\t1.4\t1.2\t1.0\t0.8\t0.7\t0.6\t0.5\t0.4\t0.3")};
        }
        return changes.empty() ? ET_OK : self.ui.patch(e->panel, changes, e->revision, e->origin, &self.completion);
    }
public:
    static constexpr std::string_view id = "org.epictuner.examples.cascading";
    static constexpr uint64_t capabilities = ET_CAP_UI | ET_CAP_LOG | ET_CAP_EVENTS;
    et_result start(Context& host) {
        context = host;
        auto result = host.check(ui.connect(host, input, this), "ui.connect cascading selects");
        if (result != ET_OK) return result;
        PanelBuilder panel("org.epictuner.examples.cascading.main", "Cascading selections");
        panel.definition.menu = panel.keep("Examples/Cascading selections");
        panel.add(ET_UI_COLUMN, "root");
        panel.add(ET_UI_LABEL, "notice", "root").text(ET_UI_TITLE, "Synthetic Make / Model / Tune workflow. No ECU reads or writes.");
        panel.add(ET_UI_SELECT, "make", "root").text(ET_UI_TITLE, "Make")
            .text(ET_UI_OPTIONS, "Acura\nBMW\nFord\nHonda\nLexus\nMazda\nMitsubishi\nNissan\nPolaris\nSubaru\nToyota\nVW - Audi\nYamaha").selectedIndex(-1);
        panel.add(ET_UI_SELECT, "model", "root").text(ET_UI_TITLE, "Model").text(ET_UI_OPTIONS, "").selectedIndex(-1);
        panel.add(ET_UI_SELECT, "tune", "root").text(ET_UI_TITLE, "Tune").text(ET_UI_OPTIONS, "").selectedIndex(-1);
        panel.add(ET_UI_CALIBRATION_TABLE, "preview", "root").text(ET_UI_TITLE, "Dead-time preview (synthetic)")
            .number(ET_UI_COLUMNS, 12).number(ET_UI_MINIMUM, 0).number(ET_UI_MAXIMUM, 100)
            .text(ET_UI_VALUE, "").boolean(ET_UI_READ_ONLY, true);
        return host.check(ui.define(panel, &completion), "ui.define cascading selects");
    }
};
ET_PLUGIN(CascadingSelects)
````

### examples/cascading-selects/plugin.json.in

````json
{
  "schema": 1,
  "id": "org.epictuner.examples.cascading",
  "name": "Cascading selections",
  "version": "1.0.0",
  "abi": {"major": 1, "minMinor": 1},
  "platform": "@ET_PLATFORM@",
  "entry": "cascading-selects@CMAKE_SHARED_MODULE_SUFFIX@",
  "requiredFeatures": ["et.ui.v1", "et.events.v1", "et.log.v1"],
  "optionalFeatures": [],
  "capabilities": ["ui", "events", "log"]
}
````
