Module reference · Uplink

Operator-side Assistance (uplink)

The uplink's optional third stage. Reshapes or gates the mapped command before it's transmitted — and when a study leaves it out, the command passes straight through untouched.

Layer Unite.Kernel
Direction Uplink · operator → robot
Base type OperatorSideAssistanceModule
Selection Optional — zero or one per study
Demo implementation None shipped yet
Paper §5.3

What

Operator-side Assistance (uplink) is a TeleroboticsModule that sits between Command Mapping & Encoding and Uplink Communication, with the chance to inspect, transform, or drop each mapped command before it's transmitted toward the robot. Unlike the two stages upstream of it, it's optional — a study that has nothing to add here simply leaves the field empty.

Concretely, it is OperatorSideAssistanceModule, an abstract class in Unite.Kernel (Assets/UNITE/Kernel/Uplink/OperatorSideAssistance.cs). Unlike InputProvider<TOutputContract> or CommandMappingAndEncoding<TInputContract, TCommandContract>, it isn't generic over a declared contract type — it works on the untyped Package envelope those stages already produced, because its job is to act on whatever command the study already defined, not to define a new one.

Why

UNITE's scoping review found shared control and operator-side assistance techniques — rate limiting, deadman gating, envelope constraints — to be another recurring but inconsistently instantiated part of a delayed-teleoperation apparatus, alongside input devices and control mappings. Giving assistance its own slot in the pipeline makes "did this study apply operator-side assistance, and exactly what did it do" an explicit, reportable configuration choice, rather than logic quietly folded into a mapping or a vehicle controller.

Making the slot optional, rather than requiring a pass-through implementation, keeps a baseline condition honest: a study with no assistance leaves the field empty and the Kernel wires Command Mapping & Encoding straight to Uplink Communication — there is no stub component silently doing nothing, and nothing downstream needs to know whether this stage ran. That absence is itself part of what a study reports.

How

Every tick, the Kernel drains whatever mapped-command packages arrived and calls TryProcessPackage once per package, in order, with that tick's timestamp:

C#
protected abstract bool TryProcessPackage(
    Package package,
    double timestampSeconds,
    out Package output);

Return false to drop the package for this tick — no command reaches Uplink Communication at all. This is the mechanism a deadman switch or a gating technique uses. Return true with a package to publish it downstream. Before publishing, the Kernel only checks that:

  • the returned package is not null.

That's the entire check — unlike Input Provider and Command Mapping & Encoding, there's no timestamp-equality validation here. Those two stages validate against a declared contract type with known timestamp fields; this stage works on an untyped Package, so it's the implementation's own responsibility to preserve or update SourceTimestampSeconds sensibly when it constructs a new one. Failing the null check logs an error and drops the tick. Return true with a valid package and that's the entire job done — Uplink Communication is already subscribed and picks it up automatically, exactly as if this stage weren't there.

Boundary

Owns

  • Receiving every package Command Mapping & Encoding publishes, queued in arrival order.
  • Deciding, per package, whether to drop it, pass it through unchanged, or publish a transformed replacement.
  • Publishing at most one output package per package it received.

Does not own

  • Producing the original command — Command Mapping & Encoding's job.
  • Applying delay or transmitting anything — Uplink Communication's job.
  • Reading live robot-side observations — this stage runs before the delay, with no view of remote state.
  • Interpreting the command physically, or controlling the vehicle.

Contract

Unlike Input Provider and Command Mapping & Encoding, this stage has no dedicated contract type to subclass. It reads and writes the same minimal envelope those two stages already use — Package (Unite.Core):

C#
public sealed class Package
{
    public object Payload { get; set; }
    public double SourceTimestampSeconds { get; set; }
    public string StreamId { get; set; }

    public Package(object payload, double sourceTimestampSeconds)
        : this(payload, sourceTimestampSeconds, null)
    {
    }

    public Package(
        object payload,
        double sourceTimestampSeconds,
        string streamId)
    {
        Payload = payload;
        SourceTimestampSeconds = sourceTimestampSeconds;
        StreamId = streamId;
    }
}

Payload is untyped, so an implementation has to recover the concrete command type itself — typically by pattern-matching package.Payload against the command contract it expects (for instance WheelVelocityCommand from the Command Mapping & Encoding page) and passing the package through unchanged if it doesn't match, rather than crashing. That's the same "type-checked on one side only" situation Command Mapping & Encoding's own output is in: nothing enforces the payload type automatically once it's travelling as object, so a mismatch here isn't caught until a matching check fails at runtime, not at startup.

Preserve StreamId, and watch the timestamps. The base class only checks that the package you return isn't null — everything else about it is on you. Uplink Communication routes by StreamId and rejects (logs an error, drops) any package with a missing or unknown one, so a replacement package must carry the original package.StreamId forward, not a default or omitted value. Timestamps get no such check either: neither Package.SourceTimestampSeconds nor any timestamp fields on the typed payload inside it (an EncodedCommandContract's own SourceTimestampSeconds/TimestampSeconds, for instance) are validated at this stage the way they are on the way into Command Mapping & Encoding. Construct a replacement package carelessly and the package-level timestamp can silently drift out of sync with the payload's own — nothing here will catch it.

Examples

The tabs show two common TryProcessPackage behaviors: transforming a payload and dropping a tick.

Clamps how much WheelVelocityCommand's requested wheel velocities can change from one tick to the next, so a sudden full-deflection input can't snap the robot's commanded speed instantly. It always publishes — a rewritten package on a change that exceeds the limit, or the original package unchanged otherwise:

C#
public sealed class WheelVelocitySlewLimiter : OperatorSideAssistanceModule
{
    [SerializeField] private float maxChangePerSecond = 1.5f;

    private float previousLeft;
    private float previousRight;

    protected override bool TryProcessPackage(
        Package package, double timestamp, out Package output)
    {
        output = package;

        if (!(package.Payload is WheelVelocityCommand command))
        {
            return true;
        }

        float maxDelta = maxChangePerSecond * Mathf.Max(command.DeltaTime, 0f);
        float left = Mathf.MoveTowards(previousLeft, command.RequestedLeft, maxDelta);
        float right = Mathf.MoveTowards(previousRight, command.RequestedRight, maxDelta);
        previousLeft = left;
        previousRight = right;

        if (left == command.RequestedLeft && right == command.RequestedRight)
        {
            return true;
        }

        output = new Package(
            new WheelVelocityCommand(command.Input, timestamp, left, right),
            package.SourceTimestampSeconds,
            package.StreamId);
        return true;
    }
}

Config

Operator-side Assistance (uplink) is a Unity component, so it's configured like the rest of a study: in the Inspector, not in code.

  1. Add a concrete assistance component to the agent, if the study uses one. Add a component such as WheelVelocitySlewLimiter to the same GameObject as TeleroboticsAgent. Every selected module must live on that one GameObject.
  2. Assign it to the agent's Operator Side Assistance field, or leave it empty. Unlike Input Provider, Command Mapping & Encoding, Uplink Communication, and the Vehicle/Robot Model, this field isn't required — leaving it empty is a valid, deliberate configuration. The agent wires Command Mapping & Encoding straight to Uplink Communication in that case.
  3. Configure the implementation's own settings. The base class defines no configuration surface of its own — whatever the assistance technique needs (rate limits, gate keys, envelope bounds) is the concrete implementation's own serialized fields.

Disabled is not the same as empty. Leaving the field empty and assigning a component but disabling it are not equivalent. The agent wires the subscription off the field being non-null, not off whether the component is enabled — so a disabled-but-assigned component still receives and queues every package Command Mapping & Encoding publishes. Its Step just never runs while disabled, so that queue never drains: no command reaches Uplink Communication at all, silently, for as long as it stays disabled. To get real pass-through behavior, clear the field itself rather than disabling the component.

Same GameObject, one instance. The agent's Awake validates that every assigned module — including Operator-side Assistance, if the field isn't left empty — is attached to its own GameObject; a reference to a component elsewhere in the scene fails this check and disables the agent.