Module reference · Uplink

Uplink Communication

The stage where a study configures the communication condition on the command path — the delay, and any other impairment, applied to everything travelling from operator to robot. It exists so that communication is an explicit experimental factor you can set, report and compare, instead of a constant buried in whichever controller happened to need it. Moving from a baseline to a delayed condition changes one configured field; the input device, the vehicle, the assistance and the task stay exactly as they were.

Layer Unite.Kernel
Direction Uplink · operator → robot
Condition extension point CommunicationCondition
Module UplinkCommunicationModule — sealed
Selection Required — exactly one per study
Demo implementation EveryMoveFixedDelayCondition
Requirement DR1 · Configurable communication conditions
Paper §5.4 — uplink half of DR1

What

Uplink Communication sits between the operator side of the loop and the robot side. In brief:

  • It is a required module: a study always has exactly one, and every command the stages upstream produce passes through it.
  • It holds a list of channels — one per command stream, identified by its stream id, each with its own scheduled queue.
  • A communication condition is an object that decides, per package, how long it waits and whether it is sent at all. It is optional, and configured per channel — so one stream can be degraded while another is left alone. No condition means immediate delivery.
  • The Kernel ships no delay, jitter, loss or bandwidth models. A condition is written by the study.
  • A condition either suppresses a package or returns one transmission delay.
  • The Kernel only stores and releases scheduled packages. It does not implement latest-wins, coalescing or retransmission.

The downlink uses the same mechanism. DownlinkCommunicationKernel (§5.8) is built identically and uses the same CommunicationCondition base type — the extension point is direction-agnostic. Each channel owns its own configured instance and its own state, so the two directions are degraded independently. Its page carries the same explanation: Downlink Communication.

The module itself is sealed — not abstract, not generic. You never subclass it. What a study writes, if anything, is a condition.

Why

This module is the uplink half of DR1, configurable communication conditions, the first design requirement in UNITE's scoping review. The review found delay imposed on control commands, on video, on telemetry and on force feedback. It was fixed in some studies, followed a time-varying function in others, and was sampled from a distribution in others again, sometimes alongside impairments like packet loss. The mechanism was almost never described well enough to copy.

DR1 concludes that a reported delay magnitude alone is not enough to reconstruct the condition that produced it. The requirement is therefore not "support delay" but to make five things explicit and configurable: the stream affected, the direction, the delay magnitude, its temporal behaviour, and any additional impairment. Channels supply the first, having a module on each path supplies the second, and the condition supplies the rest.

The direction is easy to underrate. Maag et al. compared 200/0, 0/200 and 100/100 ms uplink/downlink splits and found the same total delay produced different results depending on how it was allocated. A round-trip figure cannot express that, so uplink and downlink are configured separately here instead of sharing one latency setting.

Make the directional split explicit. The study UNITE reconstructs reports a fixed 2.56 s round-trip delay without stating how it divides between the two directions. The shipped demo makes that choice explicit: 1,280 ms uplink and 1,280 ms downlink. A study reproducing the paper's earlier uplink-only assumption can still configure the full delay on this module and leave downlink clean.

The module is required; the condition is not. Commands travel the same path whether or not a study degrades them, so a baseline arm is an empty Condition field rather than different code. Between two arms of a study, the only difference is the factor being manipulated.

How

The whole model is one line — package → channel for its stream → optional condition → scheduled queue → release — and only the condition belongs to the study. The rest is the same for every package on every channel.

Routing by stream id

Packages arrive by event, not by tick. Whatever stage sits upstream — Operator-side Assistance (uplink) if the study uses one, otherwise Command Mapping & Encoding — calls ReceivePackage the moment it publishes. The module trims the package's StreamId and looks it up in its channel dictionary; a null package, a blank stream id, or one with no matching channel each log an error and go no further.

Who does what: the path a package takes

Only one step of the path belongs to the study. The rest is the same for every package on every channel:

The path a package takes through a communication channel Four steps left to right: the package enters the channel; the study-provided condition computes its release time; the Kernel's time-ordered queue holds it; the Kernel releases it when due, in scheduled order. Only the second step is provided by the study. STUDY-PROVIDED Package ingress enters the channel Condition computes release time Time-ordered queue holds until due Release in scheduled order
The condition schedules a transmission; the Kernel queue performs generic storage and release. The queue never asks what model produced a release time, and the condition never sees the queue. A channel with no condition skips the middle step and schedules for immediate release.

The default: no condition, immediate delivery

An empty Condition field is the undegraded baseline. The package still uses the channel queue, but its zero delay allows immediate release.

How long, or whether: the two outcomes

When a channel does have a condition, it is asked one question per package, and there are exactly two possible answers. A condition either suppresses the package or returns one transmission delay.

C#
bool TryGetTransmissionDelay(
    Package package,
    double ingressTimestampSeconds,
    out double delaySeconds);
Returns Meaning Effect
false Not transmitted Nothing is queued, and delaySeconds is ignored. Nothing downstream is told the package went missing.
true Scheduled once The package is queued for release delaySeconds after it entered the channel.

The condition schedules at most one delivery per package; it cannot duplicate, reorder, or later revise a package already handed to the queue.

When a package is released

The queue releases due packages during the module step; a zero-delay package can also release immediately when it enters. The module adds no intentional latency, but release is checked at the next fixed tick, so the realised delay can be rounded up by at most one tick. The constant-delay example shows this.

Order of release

Packages release in ascending scheduled-release-time order, with arrival time breaking ties. A variable-delay condition can therefore reorder a stream; preserving order is a property of the chosen condition, not a guarantee of the queue.

Boundary

Owns

  • Routing each package to the channel matching its stream id, and rejecting it if none does.
  • Asking the channel's condition, if it has one, how long each package waits and whether it is sent.
  • Storing scheduled packages and releasing them, unchanged, when due.

Does not own

  • Defining any delay, jitter, loss or bandwidth model. Those are the study's.
  • Inspecting what a payload means. Conditions are payload-agnostic by design.
  • Replacing or removing a package already held in the queue, or any queue discipline — see the Boundary section.
  • Producing or shaping the command, acting on it, or the feedback path.

Contract

This stage declares no payload contract of its own. A Package goes in and the very same Package comes out, later.

What it declares is the extension point, CommunicationCondition. A condition must implement TryGetTransmissionDelay, which is handed the channel ingress timestamp so a condition modelling service capacity or queueing has what it needs. It should also override Reset() when it keeps state or randomness:

C#
[Serializable]
public abstract class CommunicationCondition
{
    // Return false to suppress the package. Return true and set delaySeconds
    // to schedule exactly one transmission. A package cannot produce
    // multiple deliveries through this contract.
    public abstract bool TryGetTransmissionDelay(
        Package package,
        double ingressTimestampSeconds,
        out double delaySeconds);

    // Called when the channel initialises. Stateful conditions clear their
    // state here so every trial starts identically.
    public virtual void Reset()
    {
    }
}
Member Kind Override it when
TryGetTransmissionDelay abstract Always — it is abstract, so every condition implements it. It is handed the channel ingress timestamp, so a condition that models a link with memory has what it needs.
Reset() virtual The condition keeps state or a random stream, so a repeated trial must start from the same place.

A condition is a plain [Serializable] class — not a MonoBehaviour, not a ScriptableObject. It is stored inside the channel by [SerializeReference], which is what lets the Inspector offer every subclass in a dropdown.

One validity rule applies to whatever the condition returns. A delaySeconds that is NaN, infinite or negative is rejected: the channel logs an error and drops the package rather than queueing it. So a condition that computes a delay arithmetically should clamp its own result — the jitter example floors its draw at zero for exactly this reason.

The Package envelope is documented in full on the Operator-side Assistance (uplink) page. StreamId is the routing key; Payload is the field to treat carefully. A condition that pattern-matches on a concrete payload type stops being reusable across streams and across the downlink. Asking every payload the same question through an interface does not — see the bandwidth example, where the condition needs a size and only the payload can supply one.

Examples

Three worked conditions: a constant delay, a variable delay, and a constrained bandwidth link.

Each tab gives the condition, then the release schedule it produces. Those schedules can be re-derived from the code, given the inputs each table states and three facts about the machinery:

  • The agent samples the clock once per tick, so every package entering a channel during one FixedUpdate shares the same ingressTimestampSeconds.
  • A release time is absolute: the channel queues the package at ingress + delaySeconds.
  • The queue releases while releaseTime ≤ now, checked on every tick and again immediately when a package is scheduled.

The examples assume the demo's 50 Hz loop producing one command per tick on wheel-velocity-command, with ticks landing on 0.000 s, 0.020 s, 0.040 s and so on. That last part is an idealisation for readable numbers — real timestamps come from Time.realtimeSinceStartupAsDouble and are wall-clock — but nothing about the behaviour depends on it.

The condition the first study pattern needs, and the one the demo ships — what reconstructs a paper reporting a single delay figure. It always returns true with the same delay, keeps no state, and never needs the ingress timestamp it is handed:

C#
[Serializable]
public sealed class EveryMoveFixedDelayCondition : CommunicationCondition
{
    [SerializeField] private EveryMoveCommunicationConfiguration configuration;
    [SerializeField, Min(0f)] private float fallbackDelayMilliseconds = 2560f;
    [SerializeField] private bool useConfiguredDownlinkDelay;
    [SerializeField] private float channelDelayOverrideMilliseconds = -1f;

    public override bool TryGetTransmissionDelay(
        Package package,
        double ingressTimestampSeconds,
        out double delaySeconds)
    {
        float delay = fallbackDelayMilliseconds;

        if (channelDelayOverrideMilliseconds >= 0f)
        {
            delay = channelDelayOverrideMilliseconds;
        }
        else if (configuration)
        {
            delay = useConfiguredDownlinkDelay
                ? configuration.downlinkDelayMilliseconds
                : configuration.uplinkDelayMilliseconds;
        }

        delaySeconds = delay / 1000d;
        return true;
    }
}

It never touches package, always adds exactly one delay, and reads that delay from a shared configuration asset — so a whole study's delay lives in one file that can be swapped and versioned. useConfiguredDownlinkDelay flips which field of the asset it reads, which is how the same class serves a downlink channel.

Set channelDelayOverrideMilliseconds to 250 and the first three commands of a trial go through like this:

Constant delay of 250 ms on wheel-velocity-command.
Command Ingress Delay returned Scheduled release Released on tick Realised delay
1 0.000 0.250 0.250 0.260 260 ms
2 0.020 0.250 0.270 0.280 260 ms
3 0.040 0.250 0.290 0.300 260 ms

The stream is shifted, not distorted: the commands still arrive 20 ms apart, in the order they were issued, and the robot simply starts and stops a quarter-second late. Shifting the operator's experience without distorting the command stream is what makes a constant delay usable as an experimental factor.

The realised delay is 260 ms, not the 250 ms configured. Every package is rounded up to the next tick by the same amount, because 250 is not a multiple of the 20 ms tick. Configure a multiple of the tick and the two figures agree; configure anything else and the number in your methods section is not the number the apparatus produced.

Stateless and stateful conditions. The constant-delay condition ignores ingressTimestampSeconds entirely and needs no Reset(). The other two model a link with memory — a random stream, a busy-until time — so they need the timestamp, and they must clear what they remember in Reset() so every trial starts from the same place.

One study, several streams

A condition is configured per channel, so a study is not choosing one degradation for the whole apparatus. It can degrade the stream its question is about and leave the others alone, so that any effect is attributable to that stream:

One uplink module, three command streams, three different answers.
Stream Id Condition What the study is doing with it
wheel-velocity-command EveryMoveFixedDelayCondition The manipulated factor: the delay this study is about.
gripper-command — empty — A deliberate control. Left undegraded so any effect is attributable to the driving commands, not to the manipulator.
camera-pan-command JitterDelayCondition A second, independent factor — a differently behaving link on a stream the operator uses differently.

The demo publishes wheel-velocity-command. A study can add other stream ids when it needs them. Each channel holds its own condition instance with its own state and its own random stream, so nothing on one stream can reach another. Combined with the downlink module, which is configured separately, that is how the directional allocation in the second study pattern is expressed — one budget, divided explicitly, with everything else held still.

Config

Uplink Communication is a Unity component, so it's configured like the rest of a study: in the Inspector, not in code.

  1. Add the module to the agent. Add UplinkCommunicationModule to the same GameObject as TeleroboticsAgent. Every selected module must live on that one GameObject.
  2. Assign it to the agent's Uplink Communication field. This field is required, alongside Input Provider, Command Mapping & Encoding and the Vehicle/Robot Model.
  3. Add one channel per command stream. The Channels list starts empty and at least one entry is required. Type the stream id to match exactly what Command Mapping & Encoding publishes on its Output Stream Id field — in the demo, wheel-velocity-command.
  4. Choose a condition, or leave it empty. The Condition field is a [SerializeReference] slot, so the Inspector lists every CommunicationCondition subclass in your project — the Kernel contributes none. Leaving it empty is a valid, deliberate configuration: that channel delivers immediately.

Each entry in the Channels list has exactly two fields:

Field Type Description
Stream Id string Required The stream this channel transports. Trimmed, then matched exactly and case-sensitively against the package's own StreamId. Two channels sharing an id is a startup failure.
Condition CommunicationCondition Optional The degradation applied to this stream. Empty means immediate delivery, in arrival order. Each channel holds its own instance, so two channels using the same class keep independent state.

Same GameObject, one instance. The agent's Awake validates that every assigned module is attached to its own GameObject, and the module carries [DisallowMultipleComponent]. Multiple streams are expressed as multiple channels on one module, never as multiple modules.