# VOLTRA User Live Training Stream Schema

The public stream contract is `voltra.training.v7`. Each line is one complete
JSON object. The machine-readable contract is
[`stream-schema.json`](stream-schema.json).

## Scope

The first public stream implementation covers exactly these modes:

- `weightTraining` (including chains, eccentric, assist, and burnout settings);
- `resistanceBand`;
- `damper`;
- `isokinetic`.

The command accepts Isometric, Professional/Custom Curve, Rowing, Ski, Sled Pull,
and Twin/coworker device states, but their telemetry is not part of this contract.
While the device is in one of those states, the live subscription remains active
and the CLI emits no mode-specific public training events until a supported mode
is observed. If it starts in an unsupported mode, `streamStart` is delayed. A
cleanly ended subscription still emits `streamEnd`, which may be the only public
event when no supported mode was observed.

## Source selection

The public stream exposes normalized workout events only. It does not expose raw
high-frequency runtime samples, BLE frames, source names, or protocol values.
Legacy protocol data remain internal decoder inputs and are never emitted as
public events. A consumer must not know or handle BLE source names.

## Envelope

Every event includes:

| Field | Type | Meaning |
| --- | --- | --- |
| `event` | string | Event name listed below. |
| `schema` | string | Always `voltra.training.v7`. |
| `timestamp` | string | Host-side UTC RFC 3339 timestamp with milliseconds. |
| `sequence` | integer | Process-local output order, starting at `0`. |
| `streamId` | string | UUID for this stream invocation. |
| `deviceId` | string | Connected VOLTRA device identifier. |
| `connectionId` | integer | Connection generation; it changes after reconnect. |

`sequence` is output order, not a device clock. `connectionId` is retained so a
consumer can treat reconnects as continuity boundaries. The weight unit declared
by `streamStart.units` remains fixed for the public stream, including after a
reconnect or a device preference change.

## Units and meanings

`streamStart.units` declares the units used by the numeric training values:

```json
{
  "weight": "lbs",
  "distance": "mm",
  "velocity": "mm/s",
  "power": "W"
}
```

Weight, force, and volume values are converted to the selected weight unit and
rounded to at most two decimal places before they are emitted. Counts, distance,
velocity, power, work, and duration fields remain integer values as defined by
the schema; consumers may format them for display as needed.

| Value | Unit | Meaning |
| --- | --- | --- |
| `baseWeight`, `chainsWeight`, `eccentricWeight`, `resistanceBandWeight`, `maxForce`, `meanForce` | `units.weight` | Configured or measured force/load. |
| `volume` | `units.weight` | Device-reported total training volume. It is an aggregate load metric, not a geometric volume; use it at the same event level instead of recomputing it from rep values. |
| `pullVolume` | `units.weight` | Pull-direction training volume. It is optional on `progress` and `setSummary` when the device does not provide that protocol field, and present on `workoutSummary`. |
| `pullDistanceMm` | `units.distance` | Cumulative pull distance; it does not increase during recovery. |
| `startPosMm`, `finishPosMm` | `units.distance` | Rep start and finish positions. |
| `peakVelocityMmPerSecond`, `maxVelocityMmPerSecond`, `meanVelocityMmPerSecond` | `units.velocity` | Cable velocity magnitude. Pull/recovery direction is given by the containing field. |
| `peakPowerWatt`, `maxPowerWatt`, `meanPowerWatt` | `units.power` | Mechanical power magnitude. Pull/recovery direction is given by the containing field. `rep.meanPowerWatt` is the device-reported mean for that directional measurement. |
| `totalWorkJ` | `J` | Device-reported total work from the validated V2 work measurement for one directional rep; omitted when unavailable. The legacy work field is not exposed. |
| `durationMs`, `restDurationMs`, `elapsedMs` | `ms` | Duration or elapsed time. |
| `setIndex`, `repIndex`, `reps`, `sets` | count | Device/workout counters; they are not measurements. |
| `level` | none | Damper level, from 1 (lightest) to 10 (heaviest). |
| `targetVelocityMmPerSecond` | `units.velocity` | Isokinetic target velocity. |

Weight values are normalized numbers, not protocol tenths. Missing values are
omitted; zero is never used to represent unknown.

## Events

### `streamStart`

Additional fields:

| Field | Meaning |
| --- | --- |
| `trainingMode` | The first supported training mode observed by the public stream. |
| `units` | Weight, distance, velocity, and power units for this stream. |

The stream does not expose an internal decoder profile. If the device starts in
an unsupported mode, `streamStart` is delayed
until a supported mode is observed. If the user changes to an unsupported mode
directly on the device while the stream is running, its mode-specific telemetry
is suppressed; a later supported `workoutState.trainingMode` resumes training
output with the same `streamId` and sequence.

The stream does not expose raw high-frequency runtime samples. Use `rep`,
`progress`, and summary events for workout data without consuming high-volume
mechanical telemetry.

### `workoutState`

Current workout state and configuration:

```text
trainingMode
workoutState
powerEnabled
baseWeight?              weightTraining only
chainsWeight?            weightTraining only
eccentricWeight?         weightTraining only
resistanceBandWeight?    resistanceBand only
level?                   damper only
targetVelocityMmPerSecond?  isokinetic only
```

`workoutState` is `idle`, `waitToBegin`, `workout`, `rest`, `ceasePower`,
`end`, or `unknown`.

### `rep`

One directional measurement for a physical rep. A physical rep can produce up to
two `rep` events: one with `direction: "pull"` and one with
`direction: "recovery"`. Both events use the same `setIndex` and `repIndex`;
recovery is not an additional repetition.

```text
setIndex
repIndex
direction              pull or recovery
valid
distanceMm              directional distance
peakVelocityMmPerSecond
peakPowerWatt
meanPowerWatt?            optional device-reported mean power
totalWorkJ
volume
durationMs
startPosMm?          optional
finishPosMm?         optional
```

`repIndex` is one-based in `rep` events. `valid` is the device validity result
for that directional measurement. Consumers should count only events with
`direction: "pull"` and `valid: true`. Pull and recovery events may arrive in
either order, so pair them by `setIndex` and `repIndex` when needed.

`totalWorkJ` is populated only from the device's V2 work field. The older raw
work field is intentionally not mapped because real-device observations showed
that it can numerically mirror average power rather than represent integrated
work in joules. If the V2 value is absent, `totalWorkJ` is omitted instead of
falling back to an ambiguous value. Use `meanPowerWatt` directly when the device
provides it; do not reconstruct average power from the legacy work field or use
mechanical work alone as calorie expenditure.

### `progress`

Compact current workout progress:

```text
setIndex
repIndex       completed reps in the current set; may be 0 before the first rep
volume
pullVolume?    pull-direction volume when provided by the device
pullDistanceMm   cumulative pull distance
elapsedMs
```

`progress` reports cumulative workout distance rather than a continuous movement
sample. `progress`, `rep`, `setSummary`, and `workoutSummary` remain
device-notification-driven events; they do not become a uniform fixed-rate
stream.

### `setSummary`

Completed/current set result:

```text
setIndex
reps
pullDistanceMm
volume
pullVolume?     pull-direction volume when provided by the device
durationMs
restDurationMs
pull
recovery
```

`pull` and `recovery` each contain `maxForce`, `meanForce`,
`maxVelocityMmPerSecond`, `meanVelocityMmPerSecond`, `maxPowerWatt`, and
`meanPowerWatt`.

### `workoutSummary`

Workout-level result:

```text
sets
reps
pullDistanceMm
volume
pullVolume       pull-direction volume
durationMs
restDurationMs
pull
recovery
```

The stream does not expose protocol min/max set references or raw payload
versions.

### `dataQuality`

Emitted only when the public stream has a transport or decoding quality issue:

| Field | Meaning |
| --- | --- |
| `kind` | `droppedData` for transport queue loss or `decodeError` for a notification that could not be decoded. |
| `count` | Cumulative count for this issue. |
| `dropped` | Number dropped in this particular transport event, when available. |

Do not synthesize reps or summaries after a quality event.

When the daemon cannot refresh mode-dependent context, or the refreshed mode
does not agree with the mode observed in a state notification, it suppresses
that unsafe state frame and records the issue in the local daemon log. This
internal context-synchronization guard is not emitted as `dataQuality` and is
not included in `streamEnd.droppedEvents`.

### `connectionLost` and `reconnected`

`connectionLost` contains a stable user-facing `reason` and `willReconnect`.
`reconnected` contains the new `connectionId`, reconnect `attempt`, and
`downtimeMs`. Treat reconnect as a data boundary.

### `streamEnd`

Contains:

```text
reason
complete
droppedEvents
decodeErrors
```

`reason` is `stopped`, `daemonEnded`, `daemonDisconnected`, or
`reconnectExhausted`. `droppedEvents` counts notification events lost by a
daemon-owned transport or deferred-notification queue. It does not include
internal context-synchronization guards. `complete` means the stream
subscriber stopped normally; it does not claim that a workout was completed.

A process or machine failure may end the file without `streamEnd`; treat EOF as
an abnormal termination.

## Consumer rules

- Do not inspect or deduplicate BLE source names.
- Do not count `direction: "recovery"` as a second rep. Pair it with the pull
  event using `setIndex` and `repIndex` when a complete physical rep is needed.
- Treat `dataQuality` and reconnect events as data boundaries.
- Do not infer missing values from zero.
- This stream is telemetry only; it does not create, upload, or reconcile
  History records.
