Channels, scenes, and coexistence

Most robot UIs that try to expose multiple "modes" run into the same problem: two modes both want to control the same actuator. OctoMY™ handles this with an explicit channel model so the UI can express the coordination instead of hiding it. A multi-channel snapshot — a coordinated combination — is a scene that the user can apply with a single click.

The intuition

Each channel is one independently-runnable behaviour stream: locomotion, arm, turret, lights. Inside a channel, exactly one stanza is active at a time (mutual exclusion). Across channels, stanzas run in parallel — unless their actuator claims overlap, in which case the activation API surfaces the conflict to the operator. A scene names a coordinated combination ("locomotion=walk-6 + arm=hold") that applies atomically.

Pre-Phase-14 readers

What was called a "stanza set" before Phase 14 is now a channel — but it's no longer derived by running a union-find over actuator overlap. Channels are now explicit, declared by the preset author. Overlap detection still exists (the activation API uses it to surface conflicts), but it's a runtime check rather than a static partition.


What's a stanza? what's a channel?

A stanza is a (lobe, actuator-mapping, input-descriptors) triple inside a preset:

A channel is a declared group of stanzas with a name and an idle-stanza fallback. The preset author assigns each stanza to a channel via stanza.channelId. Mutually-exclusive stanzas live in the same channel; concurrently-runnable stanzas live in different channels.

A robot in motion runs at most one stanza per channel. Switching the active stanza inside a channel is the user-visible way to switch behaviour modes within that domain ("locomotion: walk → stand still", "arm: hold → grab").


Why two stanzas might collide

Imagine a hexapod with 18 leg actuators plus a 4-DOF arm:

walk and stand are mutually exclusive — they're in the same channel. Same for hold and grab. But walk runs concurrently with hold (or with grab) because they touch disjoint actuators on different channels.

Cross-channel conflicts only arise when the preset author makes a mistake (e.g. a walk stanza claiming an arm actuator). The activation API catches these at the moment of activation.


Activation: Reject, Force, Negotiate

Runtime::activate(channelId, stanzaId, transitionMs, policy) is the channel-aware entry point. Before activating, it derives the target stanza's actuator claim and checks for overlap against every other currently-active channel. The conflict policy decides what happens:

Reject is the sensible default for an autonomous service driving a channel. Force is the right default for operator-initiated scene application (the operator's intent is explicit). Negotiate is the rarely-used middle ground.


Scenes: coordinated multi-channel snapshots

A scene is a named map from channelId to stanzaId. Applying a scene activates all of its assignments atomically, sorted by claim-decreasing order so channels giving up actuators always release before channels acquiring them.

scene "grabbing":
  locomotion → locomotion.idle    (releases the legs)
  arm        → grab                (acquires the arm — was on hold)
  turret     → aim                 (acquires the turret)

Activation order resolves to: locomotion first (gives up actuators), then arm and turret. Ties broken alphabetically for determinism.

The Remote-side Scene Picker activity surfaces one button per declared scene. Clicking writes an applyTrigger monotonic counter that the Agent's RuntimeCourier picks up and routes through Runtime::applyScene(sceneId, ConflictPolicy::Force). The lastAppliedScene value drives the active-scene highlight on the picker — only successful applies update it, so partial failures don't flash the highlight.

See the Author scenes howto for the editor + picker walkthrough.


Upgrade ladders

Each channel can declare an upgradeLadder — an ordered list of "richer" stanzas in the same channel. When channel A activates and forces channel B to its idle, the upgrade ladder lets B re-promote itself the moment it can: after every activation, the runtime runs rescanUpgradeLadders() which walks each idle channel's ladder upward looking for a stanza whose claim is now satisfiable.

The practical effect: autonomous services (path-follower, vision-tracker) can release a channel for an operator's explicit action, and the channel auto-promotes back once the operator's action finishes.


Where claims come from

A stanza's actuator claim is the set of actuator indices its mapping touches:

QSet<quint16> Stanza::actuatorIndices() const;

Two stanzas conflict iff their claims have non-empty intersection. There's no Jaccard or fuzzy match here — the activation API uses a hard set-intersection check.


What this enables in the UI

The post-Phase-17 Remote exposes the structure through three activities:

Each activity declares an ActivityManifest that decides when it's enabled. Manifests carry a requiredIntents field (what input shapes the active stanza must expose), a channelScope, explicit bound keys, and an absorbs glob list for the auto-rendered OptionalPortBank below the activity's primary surface.


Stanzas vs lobes vs actuators vs channels — a quick orientation

Lobe Stanza Channel Actuator
What it is A piece of control logic (C++ template) A configured use of a lobe inside a preset A declared group of mutually-exclusive stanzas A physical output (servo, motor)
How many in a preset A fixed library of types Many; the user authors them Few; the user declares them One per physical actuator
Reusable Yes — across stanzas, presets, robots Within one preset, used at most once Within one preset One-to-one with hardware
Concurrency N/A Mutually exclusive within a channel Concurrent across channels Shared between concurrent stanzas only via the activation API's conflict check

A lobe is "what to do". A stanza is "how this preset uses that lobe". A channel is "which behaviour stream this stanza belongs to". An actuator is "the actual servo on pin 9".


Why this matters beyond the UI

The same model flows through firmware codegen:

The path-following service uses the same model to write intent:ground-drive-2d values into a channel that exposes an autonomous-driving stanza, while leaving other channels (turret, arm, lights) free for the operator.


See also