Module reference · Robot
Vehicle / Robot Model
The robot-side module. It applies a released command through a configured vehicle behavior and updates the authoritative remote state — the pivot where the uplink ends and the downlink begins.
What
The Vehicle / Robot Model applies a received command through a configured vehicle behavior and updates the authoritative remote state. It sits at the robot end of the uplink: it consumes whatever Remote-side Assistance publishes, if a study assigns one, or whatever Uplink Communication releases directly, if not — and it is the only stage in the loop that turns a command into vehicle motion.
Concretely, it is
VehicleRobotModelModule, an abstract class in
Unite.Kernel
(Assets/UNITE/Kernel/Uplink/VehicleRobotModel.cs). A
study implements it by subclassing the generic
VehicleRobotModel<TCommand, TVehicleState>,
declaring its accepted command contract and the vehicle state
contract it publishes as generic parameters — the same shape
Command Mapping & Encoding uses for
TInputContract/TCommandContract.
Command Mapping determines what the operator asks the robot to do; the Vehicle Model determines what the robot actually does. Everything between those two stages — assistance, delay — can shape or delay the command, but none of it decides how the vehicle responds. That decision belongs here, and only here.
Accepting a command and applying it is only half the job. The other half is publishing what happened: this module owns the robot's authoritative state — not a prediction, not a reconstruction, the actual simulated position, orientation, and velocity the rest of the loop treats as ground truth. Every downlink stage that eventually shows the operator something — and Remote-side Assistance, reading local state before the next command — ultimately traces back to a state this module published.
Why
This module implements DR4, configurable vehicle behavior. The review found that the same operator command can produce different motion depending on the platform, steering geometry, dynamic model, actuator response, and wheel–terrain interaction. Some studies specified these in detail — a simulated large UGV with a 14-DoF model, Pacejka tires, a diesel engine, and an automatic transmission; soft-terrain slippage modeled as part of the vehicle; a deliberate distinction between the controller-internal model and the simulated plant. Others described the apparatus without specifying its motion behavior at all: a three-wheel robot with two motor-driven wheels and a caster, with no statement of the steering geometry, or game physics characterized as "realistic" without a model type or parameters. In both of the latter cases the vehicle behavior cannot be reconstructed from the description.
DR4 does not ask for one canonical vehicle model. It asks that steering geometry, kinematic or dynamic behavior, actuator response, and wheel–terrain interaction each be an explicit, reportable configuration choice — and that steering geometry in particular be a field a study sets, not a property implied by how a GameObject happens to be built. A study that reports which Vehicle Model it used and which of these fields it changed has said enough for someone else to rebuild the apparatus; a study that reports only "differential drive" or "realistic physics" has not.
What the reconstruction revealed.
The study UNITE reconstructs (§6) describes its disturbance
model in structural detail — naming six sources and
identifying which use Perlin noise — but reports no
parameter values, describing them only as "iteratively
calibrated." Reconstruction needs concrete numbers, so the
reconstruction chose its own to produce comparable
trajectory variation, reproducing the model's
behavior but not its exact calibration. Those six
sources are exactly the six disturbance fields on
TurtleBot3-WafflePi-Moon, covered under
Examples
below — what the original paper left as "iteratively
calibrated" becomes six explicit, versioned numbers here,
each one a value the reconstruction had to pick and could
instead have reported.
How
A released command's path through this module, and what happens on every tick regardless of whether one arrived:
-
A command package arrives.
TeleroboticsAgentwires exactly one upstream source to this module'sReceivePackage, decided once at startup: Remote-side Assistance's output if a study assigned one, otherwise Uplink Communication's release, directly. -
ReceivePackageonly enqueues. The public, package-receiving entry point does no work of its own — it queues the package for this tick'sStep. - Each vehicle step applies queued commands. The model checks each payload against its declared command type. A mismatched payload is rejected; a valid command is passed to the vehicle-specific implementation.
-
UpdateVehicleStatealways runs, once, last. After every queued command this tick has been applied — even if the queue was empty —Stepcallsprotected abstract void UpdateVehicleState(double timestampSeconds). State therefore refreshes on every tick at the study's configured update rate, not only on ticks that happened to receive a command. - Inside those two methods, the concrete model does whatever its vehicle behavior needs. The base class prescribes nothing here: interpreting steering geometry, integrating a kinematic or dynamic model, applying actuator response, applying wheel–terrain effects are all entirely the implementation's own — see Examples below for what the shipped rover actually does.
-
UpdateVehicleStatefinishes by publishing.protected void PublishState(TVehicleState state)checks that the state's own timestamp matches the tick's current timestamp, then setsLatestStateandHasState, and firesevent Action<TVehicleState> StateUpdated. -
Downstream, anything holding a reference reads
LatestState. Remote-side Assistance on the next tick, and Remote Observation & State Capture's sources, each pattern-matchLatestStatedown to the concrete state type they expect — exactly like a shipped observation source does forTurtleBotState.
That last point is the distinction worth keeping straight: operator-side state may be reconstructed or predicted — that is what Operator-side State Reconstruction does, downlink, after its own delay — but the Vehicle / Robot Model owns the actual authoritative remote state. Nothing else in the loop simulates the robot; everything else either commands it or observes what it already did.
The concrete state is the model-owned snapshot for that tick. It can contain pose, realized velocities, actuator values, sensor readings, or other vehicle facts. Observation sources project the fields needed by each consumer; they do not create a second authoritative vehicle state.
Boundary
Owns
- Accepting only its declared command contract, and logging an error and dropping anything else.
-
Its steering geometry, kinematic or dynamic model,
actuator response, and wheel–terrain interaction —
however
ApplyCommandandUpdateVehicleStatechoose to implement them. -
Publishing the single authoritative remote vehicle
state for the tick, exactly once, through
PublishState.
Does not own
- Reading an input device or mapping it into a command — Command Mapping & Encoding's job.
- Communication delay or transport — Uplink Communication's job.
- Deciding whether to preserve, modify, replace, or suppress a command before it arrives — Remote-side Assistance's job, if a study assigns one.
- Sampling or packaging its own state for the downlink — Remote Observation & State Capture's job; this module publishes state, it doesn't decide what a study observes from it.
Contract
A concrete model declares two things as generic parameters: the
command contract it accepts — a subclass of
EncodedCommandContract, documented in full on the
Command Mapping & Encoding page — and the state contract it publishes, a subclass of
VehicleStateContract:
public abstract class VehicleRobotModelModule : TeleroboticsModule, IPackageConsumer
{
public abstract void ReceivePackage(Package package);
/// <summary>Whether a state has been published yet.</summary>
public bool HasState { get; protected set; }
/// <summary>
/// The most recently published state. A module that only holds a reference to
/// this Kernel base type (rather than the concrete vehicle model) reads state
/// through here and pattern-matches down to the concrete state type it expects.
/// </summary>
public VehicleStateContract LatestState { get; protected set; }
}
public abstract class VehicleRobotModel<TCommand, TVehicleState>
: VehicleRobotModelModule
where TCommand : EncodedCommandContract
where TVehicleState : VehicleStateContract
{
public event Action<TVehicleState> StateUpdated;
protected abstract void ApplyCommand(TCommand command, double timestampSeconds);
protected abstract void UpdateVehicleState(double timestampSeconds);
protected void PublishState(TVehicleState state)
{
// ...rejects null, rejects a timestamp that doesn't match the current
// tick, then sets LatestState/HasState and fires StateUpdated...
}
}
VehicleStateContract (Unite.Core) is the
published-state counterpart of EncodedCommandContract
— one timestamp, validated the same way:
public abstract class VehicleStateContract
{
public double TimestampSeconds { get; }
protected VehicleStateContract(double timestampSeconds)
{
// ...validates it is finite and non-negative...
TimestampSeconds = timestampSeconds;
}
}
A study implements the two abstract methods and constructs its
own TVehicleState in
UpdateVehicleState, whose
TimestampSeconds must equal the tick's own timestamp
before PublishState will accept it —
Examples, below,
shows the shipped rover doing exactly this.
public sealed class TurtleBotSnapshot
{
public VehicleKinematics Kinematics { get; }
public VehicleActuation Actuation { get; }
public VehicleSensors Sensors { get; }
}
public sealed class VehicleActuation
{
public float LeftVelocity { get; }
public float RightVelocity { get; }
public float SteeringAngle { get; }
}
public sealed class VehicleSensors
{
public CameraSensorSnapshot Camera { get; }
public IReadOnlyList<DistanceSensorSnapshot> DistanceSensors { get; }
}
public sealed class CameraSensorSnapshot
{
public Vector3 Position { get; }
public Quaternion Orientation { get; }
public CameraFrameSnapshot CapturedFrame { get; }
}
public sealed class DistanceSensorSnapshot
{
public string SensorId { get; }
public Vector3 Origin { get; }
public Vector3 Direction { get; }
public float Distance { get; }
public float MaximumDistance { get; }
public bool HasHit { get; }
}
public sealed class TurtleBotState : VehicleStateContract
{
public TurtleBotSnapshot Snapshot { get; }
}
Complete state. PublishState publishes the full TurtleBotSnapshot; Remote Observation & State Capture selects the fields that become robot-pose, robot-state, or robot-view.
Vehicle-specific shape. Another vehicle defines the state facets it needs, applies its command-to-motion rule, updates realized actuation, reads mounted sensors, and publishes one complete snapshot without changing the Kernel module.
Kinematics and actuation. Kinematics describes motion such as position, orientation, and velocity. Actuation describes the mechanism that produced it, such as realized wheel velocities or a steering angle; those values may differ from the requested command.
Mounted camera. CapturedFrame is the camera exposure handle associated with the snapshot. RobotViewSource uses it to create robot-view while the Vehicle Model remains the camera owner.
The command type a study picks in Command Mapping & Encoding
determines which Vehicle Model can consume the resulting package.
That page's own three examples —
differential drive, skid-steer, and position delta — are three different TCommand shapes, and each
needs a Vehicle Model whose generic parameter matches. The shipped
study scene uses differential drive: TurtleBot3WafflePiEnhanced
declares WheelVelocityCommand. Selecting a skid-steer
or position-delta mapping without a Vehicle Model built for its
command leaves every package failing the check below, from the
very first tick.
Type-checked on one side only.
Like every stage downstream of Uplink Communication, this
module never sees TCommand guaranteed by the
type system — packages travel as an untyped payload, so
Step pattern-matches package.Payload
against TCommand itself and logs an error on a
mismatch rather than crashing. A study that changes its
command contract upstream — swapping Command Mapping &
Encoding's output type — has to pick a Vehicle Model built
for the new type, or every command silently fails this
check for the rest of the trial.
Steering geometry is a
configuration field, not an inferred property.
DR4 asks specifically that steering geometry be something a
study sets explicitly — a serialized field or a chosen
implementation class — rather than a property implied by how
a GameObject happens to be constructed. Two implementations
that both accept
WheelVelocityCommand can integrate it completely
differently; nothing about the command contract itself
says which one a study is using.
Examples
The demo follows one command all the way from the existing differential-drive
mapping to the rover's authoritative state. The mapping creates a
WheelVelocityCommand; the Vehicle Model turns those requested
wheel velocities into motion, then publishes the resulting pose and wheel
state.
ArrowToWheelVelocityMapping
SlopeBoundaryGuard — actually sits between Uplink
Communication and this module too; see
What, above. The chain here
starts from the command's origin and skips to what this
section is about: how the Vehicle Model interprets it.
Differential drive: command to motion.
ArrowToWheelVelocityMapping has already converted the
operator's input into left and right wheel velocities. After the uplink
delay, TurtleBot3WafflePiEnhanced consumes that command.
public sealed partial class TurtleBot3WafflePiEnhanced
: VehicleRobotModel<WheelVelocityCommand, TurtleBotState>
{
[SerializeField] private EveryMoveVehicleConfiguration configuration;
[SerializeField] private Transform robot;
[SerializeField] private Transform robotCamera;
[SerializeField] private Camera cameraSensor;
[SerializeField] private MeshCollider terrain;
private float x, z, theta, vL, vR;
protected override void ApplyCommand(WheelVelocityCommand command, double timestamp)
{
float dt = command.DeltaTime;
vL = Mathf.MoveTowards(vL, command.RequestedLeft, configuration.maxLinearAcceleration * dt);
vR = Mathf.MoveTowards(vR, command.RequestedRight, configuration.maxLinearAcceleration * dt);
ApplyDisturbances(ref vL, ref vR, dt);
float difference = Mathf.Clamp(vR - vL,
-configuration.maxWheelVelocityDifference, configuration.maxWheelVelocityDifference);
float velocity = (vL + vR) * 0.5f;
x += velocity * Mathf.Cos(theta) * dt;
z -= velocity * Mathf.Sin(theta) * dt;
theta += difference / configuration.wheelbase * dt;
}
protected override void UpdateVehicleState(double timestamp)
{
Vector3 position = new Vector3(x, robot.position.y, z);
robot.position = position;
robot.rotation = Quaternion.Euler(0f, theta * Mathf.Rad2Deg, 0f);
Vector3 velocity = new Vector3(
(vL + vR) * 0.5f * Mathf.Cos(theta),
0f,
-(vL + vR) * 0.5f * Mathf.Sin(theta));
// The vehicle-owned camera sensor supplies the latest frame handle.
PublishState(new TurtleBotState(timestamp, new TurtleBotSnapshot(
new VehicleKinematics(position,
Quaternion.Euler(0f, theta * Mathf.Rad2Deg, 0f),
velocity, theta),
new VehicleActuation(vL, vR, latestInput),
new VehicleSensors(robotCamera
? new CameraSensorSnapshot(
robotCamera.position,
robotCamera.rotation,
latestCameraFrame)
: null)));
}
}
The model uses differential-drive kinematics: the average of the two
realized wheel velocities advances the rover, while their difference
updates its heading using the configured wheelbase. There is no
Rigidbody, WheelCollider, or PhysX drive component
in this model. Mathf.MoveTowards models actuator response and
ApplyDisturbances applies the configured wheel slip and
terrain-sampled vibration from
TurtleBot3-WafflePi-Moon.
The model applies the computed position and orientation to the robot
transform before publishing the state. PublishState then
commits that same tick's result for other modules to read. The snapshot
also records the mounted camera pose and the latest vehicle-owned camera
frame handle; Remote Observation & State Capture only selects and
packages that sensor data into streams.
Study example.
Compare operator-side assistance or presentation techniques
while keeping the vehicle model, terrain, task, and
communication delay fixed. This is exactly what the demo's
four scenes — condition-baseline,
condition-network,
condition-path, and
condition-envelope — already do: they share the
same TurtleBot3-WafflePi-Moon vehicle, delay,
and task configuration, and differ only in the downlink
presentation module under test.
Sensor records. The TurtleBot snapshot contains the mounted camera sensor. A vehicle with other sensors adds one typed record per sensor and publishes those readings with the same tick's kinematics and actuation.
Here PublishState stores the complete
vehicle state, including the sensor records defined by that vehicle.
Remote Observation & State Capture can publish different streams from
that state. A study-specific local module can read a local state stream
while the operator receives only the video stream. In the current
TurtleBot reconstruction, SlopeBoundaryGuard reads
robot-pose; robot-state has no consumer:
| Stream | Consumer | Contents | Path |
|---|---|---|---|
robot-state |
No current demo consumer | Pose and heading; sensor records when needed | Local; no communication delay |
robot-view |
Operator | Camera frames | Downlink Communication; delayed |
A vehicle adds its sensor records before publishing the snapshot:
public sealed partial class TurtleBot3WafflePiEnhanced
: VehicleRobotModel<WheelVelocityCommand, TurtleBotState>
{
protected override void UpdateVehicleState(double timestamp)
{
// ...apply the simulated position and orientation...
var snapshot = new TurtleBotSnapshot(
kinematics,
actuation,
sensors);
PublishState(new TurtleBotState(timestamp, snapshot));
}
}
The Vehicle Model owns sensor readings. Remote Observation & State Capture decides which streams expose or record them, and each consumer receives only the stream it needs.
Config
Vehicle / Robot Model is a Unity component, configured in the Inspector like the rest of a study.
-
Add a concrete vehicle model to the agent.
Add a component such as
TurtleBot3WafflePiEnhancedto the same GameObject asTeleroboticsAgent. Every selected module must live on that one GameObject. - Assign it to the agent's Vehicle Robot Model field. It's required — the agent logs "TeleroboticsAgent requires one Input Provider and one Command Mapping and Encoding module, one Uplink Communication module, and one Vehicle/Robot Model" and disables itself if this field, Input Provider, Command Mapping & Encoding, or Uplink Communication is left empty.
-
Select and assign its vehicle implementation asset.
TurtleBot3WafflePiEnhancedreads its ownEveryMoveVehicleConfigurationScriptableObject (TurtleBot3-WafflePi-Moon.assetin the demo) — steering geometry, actuator limits, and wheel–terrain disturbance all live on that one shareable, versionable asset, not hardcoded in the component. -
Confirm the upstream command contract matches.
Whatever Command Mapping & Encoding — or Remote-side
Assistance, if it replaces the command — produces has to be the
exact
TCommandthis model's generic parameter declares, or every package fails the type check in How and is silently dropped for the rest of the trial. -
Confirm downstream observation modules read this
model's state.
A source such as
PoseSourceholds its own[SerializeField] VehicleRobotModelModule vehiclereference and readsvehicle.LatestState; point it at the same GameObject's Vehicle / Robot Model, or Remote Observation & State Capture has nothing to publish. - Keep every other module fixed when comparing vehicle models. The demo's own four scenes hold input, mapping, delay, terrain, and task identical and vary only the module under test — apply the same discipline when the module under test is this one.
Same GameObject, one instance.
The agent's Awake validates that every selected
module — including the Vehicle / Robot Model — is attached
to its own GameObject; a reference to a component elsewhere
in the scene fails this check and disables the agent.