Channels and Scenes
Data model and Studio UI for concurrent control modes
Reference for the Channel and Scene types in a OctoMY™ preset — the data model that lets a single robot run multiple behaviour streams (locomotion, arm, turret, lights) in parallel and apply coordinated multi-channel snapshots with one click.
One-sentence summary
A channel is a declared group of stanzas with exactly one active at a time; a scene names a coordinated
(channel → stanza)map that activates atomically. The Studio's Scenes tab inside the preset editor is where scenes are authored.
For the conceptual model and motivation, see Channels, scenes, and coexistence. This page is the precise field-by-field reference.
The Channel type
struct Channel {
QString id; // stable identifier, e.g. "locomotion"
QString name; // display name
QString defaultActiveStanzaId; // activates on boot
QString idleStanzaId; // synthesised if empty
QVector<QString> upgradeLadder; // optional auto-promotion targets
};
Fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Stable identifier used as the wire-format key (e.g. in stanza.channelId, in scene assignments, in Runtime::activate(channelId, …)). Stanza ids may not collide with channel ids. |
name |
string | no | Human-readable display name. Falls back to id if empty. |
defaultActiveStanzaId |
string | no | The stanza that activates when the preset loads. Empty → the channel boots into idleStanzaId. |
idleStanzaId |
string | no | The "rest" stanza the channel reverts to when no other stanza is active or when conflict resolution force-demotes it. Empty triggers synthesis of a no-op idle stanza named <channelId>.idle (lobe type idle, no actuator claims, no input ports). |
upgradeLadder |
array<string> | no | Stanza ids the negotiate-policy walks when a higher-priority channel needs to free actuators. See Auto-promotion. |
JSON shape
{
"id": "locomotion",
"name": "Locomotion",
"defaultActiveStanzaId": "walk-6",
"idleStanzaId": "locomotion.idle",
"upgradeLadder": ["stand", "walk-4", "walk-6"]
}
Invariants
- One active stanza per channel. There is no "no stanza active" state —
idleStanzaId(declared or synthesised) covers the rest case. - Stanza membership is declared, not inferred. Each
Stanza.channelIdnames exactly one channel; a stanza cannot float between channels. - Channels are independent. No cross-channel coordination primitives exist beyond actuator-claim conflict checks. If two channels need to coordinate, do it at the producer side (the activity, the autonomous service, etc.).
- Claim disjointness is enforced at activation, not authoring time. A preset may contain channels whose stanzas would collide — the conflict surfaces when
Runtime::activatetries to run them together.
The Scene type
struct Scene {
QString id;
QString name;
QHash<QString /*channelId*/,
QString /*stanzaId*/> assignments;
};
Fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Stable wire-format key. The Remote-side scene picker indexes by id; renaming via the UI changes name but never id. |
name |
string | no | Display name shown on the Remote scene-picker button. Falls back to id. |
assignments |
object<channelId, stanzaId> | yes | Per-channel stanza picks. Omitting a channel from assignments means fall back to that channel's idle stanza on apply — explicit, no hidden default. |
JSON shape
{
"id": "cruise",
"name": "Cruise",
"assignments": {
"locomotion": "walk-6",
"arm": "hold",
"lights": "running-lights"
}
}
Validation rules
A preset that fails any of these is rejected at load time (Preset::fromJson populates the errors list):
- Every
channelIdinassignmentsnames an existing channel. - Every
stanzaIdinassignmentsnames an existing stanza. - The named stanza's
channelIdmatches the assignment key (no cross-channel borrowing). - The combined actuator claim across all assigned stanzas is conflict-free.
Live validation in the Studio Scenes tab surfaces these errors as the operator picks dropdowns — they never get to save a preset whose scenes wouldn't load.
Authoring scenes in Studio
Scenes live inside the preset, so the Studio's preset editor tab strip owns the editing UI. Open a preset from the Preset Manager and click the Scenes tab — it sits alongside Actuators / Sensors / Lobes / Stanzas.
The tab is a three-column layout:
- Left: the scene list with Add / Remove / Rename buttons.
- Right: the assignment grid — one row per channel in the preset, each row a dropdown of in-channel stanza ids (plus the empty entry that means "use this channel's idle stanza").
- Bottom: the live validation indicator. Green for a conflict-free scene; red with the specific issue otherwise (unknown channel, foreign stanza, claim conflict).
Edits autosave through the same path as every other preset edit — there is no save button on the tab. Use the OS-level Back button when you're done.
For the full step-by-step walkthrough see Author scenes.
Applying scenes at runtime
The agent-side runtime exposes one entry point:
ActivateResult Runtime::applyScene(const QString &sceneId, ConflictPolicy policy);
The implementation is equivalent to calling Runtime::activate(channelId, stanzaId, 0, policy) for every assignment in scene.assignments, in claim-decreasing order — channels claiming more actuators are activated first so the negotiate-policy has the most freedom to demote conflicting channels along their upgrade ladders. Any channel not named in assignments falls back to its idleStanzaId.
The Remote-side ScenePickerWidget calls applyScene via the fixo-runtime SyncSection's activeSceneId field. See fixo-runtime for the full ABI.
Auto-promotion
The upgradeLadder on a channel names stanzas the runtime is allowed to demote toward when conflict resolution under ConflictPolicy::Negotiate runs.
Example — a hexapod's locomotion channel:
{
"id": "locomotion",
"upgradeLadder": ["stand", "walk-4", "walk-6"]
}
walk-6 claims 18 leg actuators; walk-4 claims 12; stand claims 0. If the arm channel tries to activate a stanza that needs 4 of the leg actuators, the negotiator walks locomotion's ladder from its current position toward index 0 until the claim shrinks enough to fit. A channel without a ladder doesn't auto-promote — it requires explicit user-driven downgrade.
The ladder is purely advisory data. The runtime never auto-upgrades (walks toward higher indices on its own); promotion always happens under operator command or scene apply.
Conflict policy
Both Runtime::activate and Runtime::applyScene take a ConflictPolicy:
| Policy | Behaviour |
|---|---|
Reject |
Return ok=false with the conflict list if any actuators overlap. The current activation is unchanged. |
Force |
Move every conflicting channel's active stanza to its idle stanza, then proceed. |
Negotiate |
Walk each conflicting channel's upgradeLadder until conflict resolves; if no ladder entry resolves it, return ok=false. |
Operator-driven activations from the Remote default to Reject — surfacing conflicts is the whole point of the channel model. Scene applies typically default to Force so a "cruise" scene reliably reaches its declared state. Autonomous services pick per-use-case.
Pre-Phase-14 readers
Before Phase 14, what is now a channel was called a stanza set and was computed by running a union-find over actuator overlap across all stanzas. Channels are now declared by the preset author via Stanza.channelId; overlap detection still happens but at runtime, not as a static partition.
The data-model migration is one-way — the JSON IO no longer reads the old stanzaSets field. The migration tool that ships with Phase 14 fixtures is the canonical upgrade path for existing presets.
See also
- Channels, scenes, and coexistence — the why behind the model.
- Stanzas — what runs inside a channel.
- Stanza Templates — reusable stanza patterns.
FixoRuntime— the runtime that holds the active stanza per channel and servesapplyScene.- Author scenes — the step-by-step howto.