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.
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 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.
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:
-
Stepreleases every package that is due on each channel. -
ReceivePackagealso 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:
[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.
condition-baseline → delayed-video baseline. The robot sends robot-pose and robot-view through the same downlink channels. No Operator-side Assistance (downlink) is selected, so the operator receives the delayed feedback without a predictive overlay.
| Downlink stage | Selected implementation | Operator receives |
|---|---|---|
| Communication | EveryMoveFixedDelayCondition | Configured delayed pose and video |
| Operator-side Assistance (downlink) | None | No overlay |
condition-network → command-timing visualization. Communication and vehicle behavior stay unchanged. CommandTimelineAssistance adds the command timeline so the operator can see when commands were sent and released.
| Downlink stage | Selected implementation | Operator receives |
|---|---|---|
| Communication | EveryMoveFixedDelayCondition | Same delayed pose and video |
| Operator-side Assistance (downlink) | CommandTimelineAssistance | Command-timing visualization |
condition-path → trajectory visualization. IdealTrajectoryAssistance starts from the state associated with the delayed video frame and draws the predicted centre and wheel-offset paths from the command history.
| Downlink stage | Selected implementation | Operator receives |
|---|---|---|
| Communication | EveryMoveFixedDelayCondition | Same delayed pose and video |
| Operator-side Assistance (downlink) | IdealTrajectoryAssistance | Predicted centre, left-wheel, and right-wheel paths |
condition-envelope → uncertainty visualization. WorstCaseEnvelopeAssistance uses the configured vehicle disturbance parameters to draw an uncertainty envelope around the predicted motion.
| Downlink stage | Selected implementation | Operator receives |
|---|---|---|
| Communication | EveryMoveFixedDelayCondition | Same delayed pose and video |
| Operator-side Assistance (downlink) | WorstCaseEnvelopeAssistance | Uncertainty envelope around predicted motion |
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:
| 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.
-
Add the module to the agent.
Add
DownlinkCommunicationKernelto the same GameObject asTeleroboticsAgent. Every selected module must live on that one GameObject. - 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.
-
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-poseandrobot-view. -
Choose a condition per channel, or leave it empty.
The Condition field is a
[SerializeReference]slot, so the Inspector lists everyCommunicationConditionsubclass 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.