Channels and Scenes

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

  1. One active stanza per channel. There is no "no stanza active" state — idleStanzaId (declared or synthesised) covers the rest case.
  2. Stanza membership is declared, not inferred. Each Stanza.channelId names exactly one channel; a stanza cannot float between channels.
  3. 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.).
  4. Claim disjointness is enforced at activation, not authoring time. A preset may contain channels whose stanzas would collide — the conflict surfaces when Runtime::activate tries 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):

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:

  1. Left: the scene list with Add / Remove / Rename buttons.
  2. 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").
  3. 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