Module reference · Uplink
Remote-side Assistance
The uplink's optional robot-side stage. Processes commands after the uplink delay, using current local observations before the vehicle applies them.
What
Remote-side Assistance is a TeleroboticsModule that
sits between Uplink Communication and the Vehicle/Robot Model.
It is the last stage that can inspect, transform, or drop a
command before the vehicle executes it — right up to the moment
of execution — and the first stage in the loop that runs on the
robot side, after the uplink delay.
Like Operator-side Assistance (uplink), it's optional: a study that has
nothing to add here leaves the field empty and a released
command goes straight to the Vehicle/Robot Model. What sets it
apart is that it can also read the robot's
current local observations — the latest
robot-side snapshot, such as pose, terrain, or sensor state —
before deciding what to do with a command.
Concretely, it is UplinkRemoteSideAssistanceModule,
an abstract class in Unite.Kernel
(Assets/UNITE/Kernel/Uplink/UplinkRemoteSideAssistance.cs).
It uses the same package-processing shape as Operator-side
Assistance, plus one addition only this side of the delay
needs: a second receive path,
ReceiveObservation, and a read method,
TryGetLatestObservation — both covered under
Contract below.
Why
DR5 does not treat assistance as one slot. The review found shared control, safeguarded execution, delay compensation, and obstacle avoidance recurring at a specific point: after a command has crossed the uplink delay, but before it reaches the vehicle. That is a different position in the loop than Operator-side Assistance (uplink), which never sees the delay or any remote state at all. Giving that position its own module turns it into an explicit, reportable configuration choice: did this study apply obstacle avoidance, shared control, or delay compensation on the robot side, and exactly what did it read to decide? Without a dedicated module, that logic would otherwise be folded into the vehicle controller.
The contrast that matters for study design:
Operator-side Assistance (uplink) acts before the command
crosses the delay; Remote-side Assistance acts
after the delay, using the robot's current state. That
capability comes from TryGetLatestObservation.
Whatever it returns is the robot's current state — not the state
the operator was looking at when they issued the command, and
not the state at the moment the command was released either, but
whatever Remote Observation & State Capture last sampled
locally, without crossing any delay. That is what lets this
stage react to terrain, obstacles, or vehicle state that has
changed since the operator acted — something Operator-side
Assistance cannot structurally do.
What the reconstruction revealed. The paper attributes course keeping to "cliffs and boundaries" without specifying whether the behavior belongs to the scene or an assistance mechanism. UNITE represents it as an explicit, configurable Remote-side Assistance choice; the slope-based guard appears in the Examples.
The module is optional; the dependency is not implied. Assigning a concrete implementation here does not, by itself, wire up any sensor or state feed — an implementation declares what it reads (a stream id, typically), and the study is responsible for making sure something actually publishes to that stream through Remote Observation & State Capture. See Config.
How
The path a released command takes, and where local observations join it:
TryGetLatestObservation reads.
A delayed command can therefore be checked against the robot's current situation before it is applied.
Every tick, the Kernel drains whatever released command packages
arrived from Uplink Communication and calls
TryProcessPackage once per package, in order, with
that tick's timestamp — identical in shape to Operator-side
Assistance's contract:
protected abstract bool TryProcessPackage(
Package package,
double timestampSeconds,
out Package output);
Return false to drop the package for this tick — no
command reaches the Vehicle/Robot Model at all. Return
true with a package to publish it downstream; the
Kernel only checks that the returned package isn't
null, exactly as it does for Operator-side
Assistance. Failing that check logs an error and drops the tick.
Reading local state is a separate, event-driven path,
independent of the command path above. Remote Observation &
State Capture continuously maintains a snapshot of the robot's
surroundings — pose, terrain, sensor readings, whatever a study
configures — and makes the latest snapshot available to this
stage without it ever crossing the uplink delay. Concretely,
that snapshot arrives through this module's
ReceiveObservation, and an implementation reads it
back through TryGetLatestObservation. Remote
Observation & State Capture refreshes and publishes that
snapshot before this module processes commands, so whatever
TryProcessPackage reads is that tick's freshly
captured local state, not anything that has crossed the uplink.
Boundary
Owns
- Receiving every command package Uplink Communication releases, queued in arrival order.
- Reading whatever locally captured observations Remote Observation & State Capture publishes, by declared stream id.
- 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
- Communication delay or packet delivery — already applied by Uplink Communication before this stage ever sees a package.
- Authoritative vehicle state — the Vehicle/Robot Model updates and owns it; this stage only reads a locally captured snapshot.
- Sensor or observation capture — Remote Observation & State Capture's job; this stage consumes what that module publishes, it doesn't sample anything itself.
- Actuator execution — interpreting the command physically stays the Vehicle/Robot Model's job.
- Task completion or logging — Task Goal & Termination and Data Capture & Logging's job.
Contract
This contract matters when writing a new assistance technique —
configuring a study that already has one is covered under
Config below. Like
Operator-side Assistance (uplink), this stage has no dedicated contract
type to subclass for the command path — it reads and writes the
same Package envelope, documented in full on the
Operator-side Assistance (uplink) page. What it adds is a second, read-only path for local state.
public abstract class UplinkRemoteSideAssistanceModule
: TeleroboticsModule, IPackageSource, IPackageConsumer
{
public event Action<Package> PackageProduced;
public void ReceivePackage(Package package);
/// <summary>
/// Receives a locally captured observation package. This is a separate
/// input from command packages: a pre-control refresh is available for
/// the current assistance step, while the post-control refresh becomes
/// the observation available on the next control tick.
/// </summary>
public void ReceiveObservation(Package package);
/// <summary>
/// Reads the latest locally captured observation for a stream. The result
/// is the observation available at the remote control tick, not a delayed
/// downlink package.
/// </summary>
protected bool TryGetLatestObservation(
string streamId, out Package package);
/// <summary>
/// Reads and type-checks the latest locally captured observation.
/// </summary>
protected bool TryGetLatestObservation<TObservation>(
string streamId, out TObservation observation)
where TObservation : class;
protected abstract bool TryProcessPackage(
Package package,
double timestampSeconds,
out Package output);
}
TryGetLatestObservation returns the latest
available local snapshot for a given stream id — whatever
Remote Observation & State Capture most recently published,
or nothing if it hasn't published yet. An implementation
declares which stream it depends on, typically as a serialized
field, then reads back the latest snapshot for that stream by
id, exactly like Uplink Communication's channels do for
commands. A generic overload adds a type check on top: it
returns that same snapshot only if its payload matches the
requested TObservation, so an implementation
doesn't have to pattern-match the payload itself. If nothing has
published to that stream yet, or the study never wired a
matching source at all, the call simply returns
false — there is no error, because an unconfigured
dependency is a valid state before the first observation
arrives, not a failure.
The Vehicle/Robot Model doesn't check StreamId either.
Unlike Uplink Communication, it only pattern-matches
package.Payload against its declared command
contract and drops the tick on a mismatch — so a
StreamId error goes uncaught here, the same
"type-checked on one side only" situation the
Operator-side Assistance (uplink) contract
describes. Preserve package.StreamId and
SourceTimestampSeconds when constructing a
replacement anyway — it's still on the implementation, even
though nothing downstream enforces it.
Examples
The baseline passes commands through; the route guard reads local pose observations before the vehicle update.
Leave the field empty to pass every released command from Uplink Communication to the Vehicle/Robot Model unchanged.
-
Dependency: the robot's current observed
pose (position and heading), read through
TryGetLatestObservation<PosePackage>on therobot-posestream, and the local terrain, sampled directly from the assignedMeshCollider— falling back to the scene's own vehicleTransformonly before the first observation has arrived. -
Decision: whether the terrain directly
ahead in the commanded direction rises more steeply than
maximumSlopeDegrees— effectively, whether it's impassable. - Output: preserves the command unchanged when the terrain ahead is passable, or replaces it with reversed velocities when it isn't.
-
Research use: makes route-keeping
assistance an explicit, configured choice — the exact gap
the paper's own reconstruction (§6) found underspecified
in the study it reconstructs. Ships as the demo's actual
implementation
(
Assets/UNITE/Demo/every-move-you-make/Runtime/CommunicationAndVehicle.cs).
public sealed class SlopeBoundaryGuard : UplinkRemoteSideAssistanceModule
{
[SerializeField] private MeshCollider terrain;
[SerializeField] private Transform vehicle;
[SerializeField, Range(0f, 90f)] private float maximumSlopeDegrees = 21.80141f;
// Reference BoundaryDetection thresholds: a rise of more than 8 cm over a 20 cm
// look-ahead is treated as a wall rather than a navigable bump.
private const float CheckDistance = 0.2f;
public float MaximumSlopeDegrees => maximumSlopeDegrees;
protected override bool TryProcessPackage(Package package, double timestamp, out Package output)
{
output = package;
if (!terrain || !vehicle) return true;
if (!(package.Payload is WheelVelocityCommand command)) return true;
// Only translational commands can drive into a wall; ignore pure rotation.
float commanded = (command.RequestedLeft + command.RequestedRight) * 0.5f;
if (Mathf.Abs(commanded) < 0.001f) return true;
if (!TryGetObservedPose(
out float px,
out float pz,
out float heading))
{
return true;
}
float relativeAngle = commanded > 0f ? 0f : Mathf.PI;
if (!IsWallInDirection(px, pz, heading, relativeAngle)) return true;
// Reference behaviour on wall contact: reverse away from the obstacle instead of
// climbing it.
output = new Package(
new WheelVelocityCommand(command.Input, timestamp, -command.RequestedLeft, -command.RequestedRight),
package.SourceTimestampSeconds, package.StreamId);
return true;
}
private bool TryGetObservedPose(
out float x,
out float z,
out float heading)
{
if (TryGetLatestObservation<PosePackage>(
EveryMoveStreams.Pose,
out PosePackage observation) &&
observation.State != null)
{
x = observation.State.Position.x;
z = observation.State.Position.z;
heading = observation.State.Heading;
return true;
}
// The first control tick can precede the first published vehicle pose.
// Retain the scene transform as a startup fallback only; subsequent
// assistance decisions use the locally captured observation package.
if (vehicle)
{
x = vehicle.position.x;
z = vehicle.position.z;
heading = vehicle.eulerAngles.y * Mathf.Deg2Rad;
return true;
}
x = 0f;
z = 0f;
heading = 0f;
return false;
}
private bool IsWallInDirection(float x, float z, float heading, float relativeAngle)
{
if (!TryTerrainHeight(x, z, out float currentHeight)) return false;
float absoluteAngle = heading + relativeAngle;
float checkX = x + Mathf.Cos(absoluteAngle) * CheckDistance;
float checkZ = z - Mathf.Sin(absoluteAngle) * CheckDistance;
if (!TryTerrainHeight(checkX, checkZ, out float checkHeight)) return false;
float slopeDegrees = Mathf.Atan2(
checkHeight - currentHeight,
CheckDistance) * Mathf.Rad2Deg;
return slopeDegrees > maximumSlopeDegrees;
}
private bool TryTerrainHeight(float x, float z, out float height)
{
if (terrain.Raycast(new Ray(new Vector3(x, 1000f, z), Vector3.down), out RaycastHit hit, 2000f))
{
height = hit.point.y;
return true;
}
height = 0f;
return false;
}
}
Notice what it never does: it doesn't touch
UplinkCommunicationModule, doesn't know the
configured delay, and doesn't call into the Vehicle/Robot
Model directly. It reads one observation stream, raycasts
against the terrain it was assigned, and returns a package
— everything else about the surrounding pipeline is
invisible to it, exactly as the Boundary above describes.
Config
Remote-side Assistance is a Unity component, configured in the Inspector like the rest of a study.
-
Add a concrete assistance component to the agent, if the study uses one.
Add a component such as
SlopeBoundaryGuardto the same GameObject asTeleroboticsAgent. Every selected module must live on that one GameObject. - Assign it to the agent's Uplink Remote Side Assistance field, or leave it empty. This field isn't required — leaving it empty wires Uplink Communication straight to the Vehicle/Robot Model, exactly as described under Pass-through baseline above.
-
If the implementation reads local observations, configure Remote Observation & State Capture to publish the stream it expects.
A dependency is declared by the implementation, not by the
Kernel.
SlopeBoundaryGuard, for instance, depends on a stream calledrobot-pose— a name for "the robot's current position and heading" — and that name has to match a source configured on the agent's Remote Observation & State Capture module, or the read simply never succeeds.
An unconfigured dependency fails silently.
Assigning a component here does not by itself require
Remote Observation & State Capture to be assigned too —
the two fields are wired independently in
TeleroboticsAgent.Awake. Add a component that
reads a stream nothing publishes to, and every call to
TryGetLatestObservation simply returns
false for the rest of the trial: no error, no
warning, just an implementation whose read-side never
succeeds.
Disabled is not the same as empty.
An assigned-but-disabled module blocks every command rather
than creating pass-through behavior — its Step
never runs, so nothing reaches the Vehicle/Robot Model
while it stays disabled, silently. That's because the agent
wires the subscription off the field being non-null,
not off whether the component is enabled. To get real
pass-through, clear the field itself rather than disabling
the component.
Same GameObject, one instance.
The agent's Awake validates that every
assigned module — including Remote-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.