# 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.
