Skip to main content
The channel interface is how WASM controllers interact with robot hardware. Instead of robot-specific APIs (set_joint_velocity, set_thrust), controllers read and write through numbered channels that are defined by the robot’s manifest. The same controller pattern works across manipulators, drones, and mobile robots. This follows the ros2_control / MuJoCo / Drake pattern: named, typed, bounded channels with discovery.

How It Works

   WASM Controller                Channel Interface              Robot
  ┌──────────────┐            ┌─────────────────────┐        ┌──────────┐
  │ process(tick) │            │  Command Channels    │        │          │
  │               │──set(i,v)─▶│  [0] velocity  ±π   │──────▶│ Joint 0  │
  │               │──set(i,v)─▶│  [1] velocity  ±π   │──────▶│ Joint 1  │
  │               │            │  ...                 │        │  ...     │
  │               │            │                      │        │          │
  │               │◀─get(i)───│  State Channels      │◀──────│          │
  │               │◀─get(i)───│  [0] position  ±2π   │◀──────│ Encoder  │
  │               │            │  [1] position  ±2π   │◀──────│ Encoder  │
  └──────────────┘            └─────────────────────┘        └──────────┘
Command channels are written by the controller each tick. They carry velocity, position, or effort values to actuators. State channels are read by the controller each tick. They carry position, velocity, or effort feedback from sensors.

ChannelManifest

Every robot is described by a ChannelManifest that lists its command and state channels, along with safety limits and metadata.
pub struct ChannelManifest {
    pub robot_id: String,          // "ur5", "quadcopter", "diff_drive"
    pub robot_class: String,       // "manipulator", "drone", "mobile"
    pub control_rate_hz: u32,      // 100
    pub commands: Vec<ChannelDescriptor>,
    pub states: Vec<ChannelDescriptor>,
}

ChannelDescriptor

Each channel has a name, type, unit, limits, default value, and optional rate-of-change cap.
pub struct ChannelDescriptor {
    pub name: String,                       // "shoulder_pan_joint/velocity"
    pub interface_type: InterfaceType,      // Position, Velocity, or Effort
    pub unit: String,                       // "rad/s", "m/s", "Nm"
    pub limits: (f64, f64),                 // (min, max)
    pub default: f64,                       // safe default (usually 0.0)
    pub max_rate_of_change: Option<f64>,    // acceleration limit per tick
    pub position_state_index: Option<usize>,// paired position state channel
}
The InterfaceType enum determines what kind of value a channel carries:
TypeDescriptionTypical unit
PositionAngular or linear positionrad, m
VelocityAngular or linear velocityrad/s, m/s
EffortTorque or forceNm, N

WASM Host Functions

Controllers access channels through these host functions, imported from the command and state modules:
ModuleFunctionSignatureDescription
commandset(i32, f64) -> i32Write a value to command channel i
commandcount() -> i32Number of command channels
commandlimit_min(i32) -> f64Minimum allowed value for channel i
commandlimit_max(i32) -> f64Maximum allowed value for channel i
stateget(i32) -> f64Read the current value of state channel i
statecount() -> i32Number of state channels
command::set returns 0 on success and 1 if the value was clamped to the channel’s limits. The safety filter applies additional clamping downstream.

Built-in Manifests

roz includes factory methods for common robot types:
ChannelManifest::ur5() — 6-DOF arm6 command channels (velocity):
IndexNameLimitsUnit
0shoulder_pan_joint/velocity+/-3.14rad/s
1shoulder_lift_joint/velocity+/-3.14rad/s
2elbow_joint/velocity+/-3.14rad/s
3wrist_1_joint/velocity+/-3.14rad/s
4wrist_2_joint/velocity+/-3.14rad/s
5wrist_3_joint/velocity+/-3.14rad/s
12 state channels (6 position + 6 velocity):
IndexNameTypeUnit
0-5{joint}/positionPositionrad
6-11{joint}/velocityVelocityrad/s
Each command channel has max_rate_of_change: 0.5 (50 rad/s^2 at 100 Hz) and a position_state_index pairing it with its corresponding position state channel for position limit enforcement.

Custom Manifests with robot.toml

For robots not covered by the built-in factories, define a custom manifest in robot.toml:
[manifest]
robot_id = "my_arm"
robot_class = "manipulator"
control_rate_hz = 100

[[manifest.commands]]
name = "joint0/velocity"
interface_type = "velocity"
unit = "rad/s"
limits = [-2.0, 2.0]
default = 0.0
max_rate_of_change = 0.3

[[manifest.commands]]
name = "joint1/velocity"
interface_type = "velocity"
unit = "rad/s"
limits = [-2.0, 2.0]
default = 0.0
max_rate_of_change = 0.3

[[manifest.states]]
name = "joint0/position"
interface_type = "position"
unit = "rad"
limits = [-3.14, 3.14]
default = 0.0

[[manifest.states]]
name = "joint1/position"
interface_type = "position"
unit = "rad"
limits = [-3.14, 3.14]
default = 0.0

Source