Module reference · Downlink

Downlink Communication

The feedback path's transport stage. Everything the robot observes passes back to the operator through a per-channel time-ordered queue: packages leave in scheduled-release order, with arrival order breaking ties. When a study supplies one, a communication condition decides how long each package waits and whether it is sent at all.

Direction-specific examples. Downlink examples model robot → operator feedback; do not skip them as Uplink duplicates.

Layer Unite.Kernel
Direction Downlink · robot → operator
Condition extension point CommunicationCondition
Module DownlinkCommunicationKernel — sealed
Selection Optional — but all-or-nothing
Demo implementation EveryMoveFixedDelayCondition
Paper §5.8

What

Downlink Communication is the mirror of Uplink Communication. Same machine, opposite direction. In brief:

  • It is the feedback path's transport stage, and part of an all-or-nothing module group. Everything the robot side captures passes through it on the way back to the operator.
  • It holds a list of channels — one per feedback stream, identified by its stream id, each with its own scheduled queue. In the demo those streams are the robot's pose and its camera view.
  • A communication condition is a small object that decides, per package, how long it waits and whether it is sent at all. It is optional, and configured per channel. 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.

One extension point, two directions. CommunicationCondition is direction-agnostic: the same base type, and the same concrete class, can serve an uplink channel and a downlink channel. But each channel holds its own instance with its own state, so the two never interact and are configured independently.

The module itself, DownlinkCommunicationKernel, is sealed. You never subclass it. What a study writes, if anything, is a condition.

Why

Feedback delay is not the same experience as command delay, and a study that manipulates one is not manipulating the other.

  • Uplink delay postpones the robot's response. The operator acts, and nothing happens for a while.
  • Downlink delay postpones the evidence that the robot responded. It already moved; the operator cannot see it yet.

Only their sum is the round trip. Giving the feedback path its own transport module makes that split explicit and reportable, instead of collapsing both halves into a single "latency" figure.

It also lets a study degrade feedback streams unevenly. Video and telemetry rarely share a link budget in a real system, and here they do not have to share a condition either.

How

Each feedback package follows its stream id into one configured channel. The channel applies its optional condition, queues the package, and releases it when due.

Routing by stream id

Packages arrive by event, not by tick. Remote Observation & State Capture calls ReceivePackage the moment it publishes a feedback package.

The module trims the package's StreamId and looks it up in its channel dictionary. Three things end the journey there, each logging an error: a null package, a blank StreamId, or a stream id with no matching channel — the last logging "has no channel for feedback stream". The lookup is ordinal, so matching is exact and case-sensitive.

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

A channel with an empty Condition field is not a broken channel. It is the undegraded case, and it is what a baseline uses.

With no condition, the channel schedules the package with a delay of zero. It still goes through the same queue as every other package, so ordering behaves identically — it simply comes out again immediately, inside the same call that put it in.

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. Invalid delays (NaN, infinite, or negative) are rejected; after a package enters the queue, the condition cannot duplicate or reorder it.

When a package is released

Two things drain the queue:

  • Step releases every package that is due on each channel.
  • ReceivePackage also drains immediately, in the same call that scheduled the package.

A zero-delay channel can deliver immediately. Otherwise, due packages are released at the next fixed tick and fan out to Operator-side State Reconstruction and, when configured, Operator-side Assistance (downlink). The realised delay can therefore be rounded up by at most one tick — 20 ms at 50 Hz.

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. Pose and video also use independent queues, so their released samples may represent different remote times.

Operator-side State Reconstruction preserves each released representation's sampling metadata. A study that needs synchronized pose and video must align those streams explicitly.

Boundary

Owns

  • Routing each feedback 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.
  • Reconciling streams that arrive out of step, or deciding what the operator sees. Those are §5.9 and §5.11.

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, shared verbatim with the uplink. 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.

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 uplink. Asking every payload the same question through an interface does not. A study-defined bandwidth condition can ask an interface for the size it needs without hard-coding a concrete payload type.

Examples

These examples follow the four conditions in the paper's Every Move You Make reconstruction. All four use the same TurtleBot, input mapping, vehicle, task, environment, and feedback channels. They differ only in Operator-side Assistance (downlink): Baseline, Network, Path, or Envelope.

Downlink Communication itself stays the same in every condition: separate robot-pose and robot-view channels, each using EveryMoveFixedDelayCondition with the current demo's configured 1,280 ms downlink delay. robot-state is not part of the paper-facing feedback path.

The paper reports a fixed 2.56 s round trip and leaves its directional split under-specified. The current demo uses 1,280 ms in each direction, so the total delay matches while the split remains an explicit demo choice.

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.

Different conditions on uplink and downlink

The two communication modules are configured separately, so an experiment can degrade each direction differently — or degrade one and leave the other clean.

The shipped TurtleBot configuration uses separate channel instances and keeps the two directions independently configurable:

Shipped TurtleBot demo: configured communication channels.
Module Stream Id Condition Effect on that stream
Uplink wheel-velocity-command EveryMoveFixedDelayCondition Commands use the configured 1,280 ms uplink delay.
Downlink robot-pose EveryMoveFixedDelayCondition Poses arrive in order with the configured 1,280 ms downlink delay.
Downlink robot-view EveryMoveFixedDelayCondition Camera frames arrive in order with the configured 1,280 ms downlink delay.

Each channel has its own condition instance. In the shipped demo, all three configured channels use the same fixed-delay class, but uplink reads the configured uplink delay and the two downlink channels read the configured downlink delay. robot-state is captured locally but has no current demo consumer and is not added to Downlink Communication.

A study can assign different condition classes to different channels, but that is an extension of the shipped configuration rather than an additional TurtleBot demo behavior.

Which is also the trap. Two channels that should contend for one link budget will not, because a condition can only ever see its own channel. A study that needs a genuinely shared link has to write a condition whose instances share state deliberately — through a common ScriptableObject, say — and report that it did.

Config

Downlink 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 DownlinkCommunicationKernel to the same GameObject as TeleroboticsAgent. Every selected module must live on that one GameObject.
  2. Assign it to the agent's Downlink Communication field. This field is optional in isolation but not in practice — see the all-or-nothing warning below.
  3. Add one channel per feedback stream. The Channels list starts empty and at least one entry is required. Type each stream id to match exactly what Remote Observation & State Capture publishes — in the demo, robot-pose and robot-view.
  4. Choose a condition per channel, 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 feedback stream this channel transports. Trimmed, then matched exactly and case-sensitively against the package's own StreamId.
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.

The feedback path is all-or-nothing. Downlink Communication is optional only in the sense that a study can run with no feedback pipeline at all. If any of Remote Observation & State Capture, Downlink Communication, Operator-side State Reconstruction or Operator Presentation is assigned, all four must be.

Miss one and the agent logs "must either all be assigned or all be absent" and disables itself. Operator-side Assistance (downlink) is the exception: it stays genuinely optional.

A misconfigured module is worse than an absent one. A channel with no stream id, an unassigned channel, an empty channel list, or two channels sharing a stream id all log an error and set enabled = false.

But the agent wires this module's subscriptions on the field being non-null, not on whether the component is enabled. So a disabled module keeps receiving packages into an empty routing table, and every feedback package logs "has no channel for feedback stream …" and is dropped. If the Console is filling with that message, the real failure is the one error logged once at startup, further up the log.

Stream ids are matched exactly. A stray leading or trailing space is forgiven — both sides are trimmed. A difference in capitalisation is not, since the lookup is ordinal. Two channels with the same id is a hard startup failure rather than a last-one-wins.

Same GameObject, one instance. The agent's Awake validates that every assigned module — Downlink Communication included — is attached to its own GameObject. The module also carries [DisallowMultipleComponent], so multiple feedback streams are expressed as multiple channels on one module, never as multiple modules.