Module reference · Uplink

Command Mapping & Encoding

The uplink's second stage. Converts one declared operator input into one declared, device-independent command — nothing more.

Layer Unite.Kernel
Direction Uplink · operator → robot
Base type CommandMappingAndEncoding<TInputContract, TCommandContract>
Selection Required — exactly one per study
Demo implementation ArrowToWheelVelocityMapping
Paper §5.2

What

Command Mapping & Encoding is a TeleroboticsModule that converts one declared Input Provider contract into one declared, device-independent command contract. It is the uplink's second stage — it sits directly after Input Provider and before the optional Operator-side Assistance (uplink) and Uplink Communication.

Concretely, it is CommandMappingAndEncoding<TInputContract, TCommandContract>, an abstract class in Unite.Kernel (Assets/UNITE/Kernel/Uplink/CommandMappingAndEncoding.cs). A study implements exactly one mapping by subclassing it for a study-defined TInputContract/TCommandContract pair and filling in a single method.

Why

UNITE's scoping review found control mappings — the formulas that turn a raw operator signal into a vehicle command — to be another recurring source of cross-study variation: exact deadzones, sensitivity curves, and velocity limits are rarely reported in full. Giving that formula its own module makes it an explicit, versioned, swappable artifact of the study, rather than logic buried inside an input handler or a vehicle controller where it's easy to change by accident and hard to report completely.

It also decouples what the operator did from what it means for the robot. Input Provider's contract is device-specific — a keyboard's four keys, a wheel's steering angle. The command contract this stage produces is not: it carries no memory of which physical device produced it. That's what lets a study support several input devices that each publish a different input contract but converge on the same command type, so everything downstream — Uplink Communication, Vehicle/Robot Model — never needs to know which device the operator used. The command's own shape is a free choice too: differential-drive wheel velocities and a future position command are both just TCommandContract.

How

When the Input Provider publishes a new contract, the mapping calls TryMapAndEncode with that input and the current timestamp. If there is no new input, it does not reprocess an older one:

C#
protected abstract bool TryMapAndEncode(
    TInputContract input,
    double timestampSeconds,
    out TCommandContract output);

Return false to skip publishing this tick. Return true with a command to publish it. Before publishing, the Kernel checks that:

  • the returned command is not null,
  • the command's SourceTimestampSeconds equals the input contract's own TimestampSeconds — the moment the operator signal it was derived from was captured, and
  • the command's TimestampSeconds equals the timestamp the Kernel handed in — the moment this mapping step ran.

Either check failing logs an error and drops the tick rather than publish a malformed command. Return true with a valid command and that's the entire job done — the Kernel wraps it in a Package and publishes it; downstream, Operator-side Assistance (if selected) or Uplink Communication is already subscribed and picks it up automatically.

Design the contract first — it determines exactly what TryMapAndEncode has to construct, so deciding its shape before writing the method is what keeps that method from turning into a grab-bag of "might be useful" fields. Contract covers the command shape; Examples shows how the shipped mapping and study patterns fill it in.

Boundary

Owns

  • Reading exactly the input contract type the study declared, once per tick a new one arrives.
  • Converting that input into its declared command contract.
  • Publishing exactly one package per successfully mapped command, tagged with its output stream.

Does not own

  • Reading the operator's device directly — Input Provider's job.
  • Constraining or filtering the resulting command — Operator-side Assistance (uplink)'s job.
  • Applying delay or transmitting anything — Uplink Communication's job.
  • Interpreting the command physically, or controlling the vehicle.

Contract

Every mapping publishes a subclass of EncodedCommandContract (Unite.Core). Where an Input Provider's contract carries one timestamp, a command carries two — because a command is derived, not captured: it needs to say both when the input it came from was captured, and when the mapping itself ran:

C#
public abstract class EncodedCommandContract
{
    public double SourceTimestampSeconds { get; }
    public double TimestampSeconds { get; }

    protected EncodedCommandContract(
        double sourceTimestampSeconds, double timestampSeconds)
    {
        // ...validates both are finite and non-negative...

        if (timestampSeconds < sourceTimestampSeconds)
        {
            throw new ArgumentOutOfRangeException(
                nameof(timestampSeconds),
                timestampSeconds,
                "The encoding timestamp cannot precede its source timestamp.");
        }

        SourceTimestampSeconds = sourceTimestampSeconds;
        TimestampSeconds = timestampSeconds;
    }
}

That last check is a second invariant an Input Provider's contract doesn't have: a command can't claim to have been encoded before its own source input was captured. A study-specific subclass adds whatever command fields the robot needs, and must forward both timestamps to the base constructor unchanged. The examples below show how the shipped mapping fills that contract.

Type-checked on one side only. Upstream, this module resolves the agent's selected Input Provider against the exact TInputContract it declares — a mismatch fails fast at startup, the same mechanism covered on the Input Provider page. Downstream is different: packages travel as an untyped payload, so nothing enforces TCommandContract automatically. Whatever reads the package next checks the type itself — typically by pattern-matching the payload and logging an error if it doesn't match, rather than failing at startup. Pick the command contract carefully: a mismatch here isn't caught until something downstream tries to read it.

Examples

These examples follow the same input → command order. Differential drive and Position delta use the same keyboard input but produce different command contracts; Skid-steer uses a command suited to different kinematics.

The demo drives a differential-drive rover, so its command is a pair of wheel velocities — a shape built for the robot's actual actuators, with no memory of which keys produced it:

C#
public sealed class WheelVelocityCommand : EncodedCommandContract
{
    public float RequestedLeft { get; }
    public float RequestedRight { get; }
    public float DeltaTime { get; }
    public ArrowInputPackage Input { get; }

    public WheelVelocityCommand(ArrowInputPackage input, double encodedAt,
        float left, float right)
        : base(input.TimestampSeconds, encodedAt)
    {
        Input = input;
        DeltaTime = input.DeltaTime;
        RequestedLeft = left;
        RequestedRight = right;
    }
}

With the contract decided, the mapping does exactly three things: read the pending input, construct WheelVelocityCommand, and return true:

C#
public sealed class ArrowToWheelVelocityMapping
    : CommandMappingAndEncoding<ArrowInputPackage, WheelVelocityCommand>
{
    [SerializeField] private float requestedForwardVelocity = 0.26f;
    [SerializeField] private float requestedTurnWheelVelocity = 0.0861f;
    [SerializeField, Range(0f, 1f)] private float innerWheelScaleWhileDriving = 0.6f;

    protected override bool TryMapAndEncode(
        ArrowInputPackage input, double timestamp, out WheelVelocityCommand output)
    {
        int forward = input.Up ? 1 : input.Down ? -1 : 0;
        int turn = input.Left ? -1 : input.Right ? 1 : 0;

        float vl = forward * requestedForwardVelocity;
        float vr = forward * requestedForwardVelocity;

        if (forward != 0 && turn < 0) vr *= innerWheelScaleWhileDriving;
        if (forward != 0 && turn > 0) vl *= innerWheelScaleWhileDriving;
        if (forward == 0 && turn != 0)
        {
            vl = -turn * requestedTurnWheelVelocity;
            vr = turn * requestedTurnWheelVelocity;
        }

        output = new WheelVelocityCommand(input, timestamp, vl, vr);
        return true;
    }
}

Same input, different command. Differential drive and Position delta both consume ArrowInputPackage — the input device didn't change, the study's choice of command representation did. Skid-steer changes both the input and the kinematics, for a physically different robot. Whichever type a study picks, everything downstream has to be built for it: Vehicle/Robot Model declares its own command type as a generic parameter and checks every package against it, logging an error and skipping any package that doesn't match rather than crashing.

Config

Command Mapping & Encoding is a Unity component, so it's configured like the rest of a study: in the Inspector, not in code.

  1. Add a concrete mapping to the agent. Add a component such as ArrowToWheelVelocityMapping to the same GameObject as TeleroboticsAgent. Every selected module must live on that one GameObject.
  2. Assign it to the agent's Command Mapping And Encoding field. It's required — the agent refuses to start (logs an error and disables itself) if this field, Input Provider, Uplink Communication, or the Vehicle/Robot Model is left empty.
  3. Set the Output Stream Id, and the mapping's own settings. The base class defines one configuration field of its own — every concrete mapping gets it for free — plus whatever the mapping itself needs (velocity limits, sensitivity curves) as its own serialized fields.
Field Type Default Description
Output Stream Id string "vehicle-control" The base class's own field, not the mapping's. It's the routing key Uplink Communication uses to pick which configured channel carries this command — it must match a channel name set up there, or every package this stage produces is logged and dropped.
Requested Forward Velocity float 0.26 Demo-specific: forward wheel speed while driving straight.
Requested Turn Wheel Velocity float 0.0861 Demo-specific: wheel speed while turning in place.
Inner Wheel Scale While Driving float 0.6 Demo-specific: how much the inner wheel slows during a driving turn.

Same GameObject, one instance. The agent's Awake validates that every selected module — including Command Mapping & Encoding — is attached to its own GameObject; a reference to a component elsewhere in the scene fails this check and disables the agent.