Module reference · Uplink
Input Provider
The uplink's first stage. Reads one device's operator signal on every fixed tick and publishes it as a typed contract — nothing more.
What
An Input Provider is a TeleroboticsModule that captures one
device's operator signal each fixed tick and publishes it as a typed
snapshot. It is the entry point of the uplink — the first thing that
happens on the path from operator to robot.
Concretely, it is InputProvider<TOutputContract>, an
abstract class in Unite.Kernel
(Assets/UNITE/Kernel/Uplink/InputProvider.cs). A study
implements exactly one input device by subclassing it for a
study-defined TOutputContract and filling in a single
method.
Why
UNITE's scoping review found operator input to be one of the recurring elements of the apparatus, and that device choice and control mapping are instantiated and reported inconsistently across studies — one of the sources of variation that makes a setup hard to reconstruct or compare. Giving input capture its own module makes that choice explicit and swappable: a study can move from keyboard to gamepad, joystick, or a custom rig without touching command mapping, delay, or vehicle logic.
Splitting capture from meaning also keeps the boundary between what the operator did and what it means for the robot enforceable rather than conventional. The Kernel owns when a provider is asked to read the device — once per fixed tick, at a Kernel-provided timestamp — so every provider in every study is sampled the same way; only what gets read is left to the concrete implementation.
How
On each fixed tick, the provider calls TryCaptureInput with
the current timestamp:
protected abstract bool TryCaptureInput(
double timestampSeconds,
out TOutputContract output);
Return false to skip publishing this tick — a disconnected
device, for instance. Return true with a contract to
publish it. Before publishing, the Kernel checks that:
- the returned contract is not
null, and -
the contract's
TimestampSecondsequals the timestamp the Kernel handed in — so pass it straight through to the contract's constructor unchanged.
Either check failing logs an error and drops the tick rather than
publish a malformed contract. Return true with a valid
contract and that's the entire job done — the next stage, Command
Mapping & Encoding, is already subscribed and picks it up
automatically.
Design the contract first — it determines exactly what
TryCaptureInput has to capture, so deciding its shape before
writing the method is what keeps that method from turning into a
grab-bag of "might be useful" fields. Contract covers what a contract
must look like; Examples shows how the shipped keyboard provider fills it in.
Boundary
Owns
- Reading whatever device — or devices — it's built for, every tick it's asked to.
- Constructing its declared contract with the Kernel-given timestamp.
- Deciding whether there's a new input worth publishing this tick.
Does not own
- Interpreting what the input means to the robot — Command Mapping & Encoding's job.
- Constraining or filtering the resulting command — Operator-side Assistance (uplink)'s job.
- Transmitting anything over a communication channel — Uplink Communication's job.
- Controlling the vehicle.
Contract
Every provider publishes a subclass of
InputProviderOutputContract (Unite.Core). It's
nine lines — one property, one validated constructor:
public abstract class InputProviderOutputContract
{
public double TimestampSeconds { get; }
protected InputProviderOutputContract(double timestampSeconds)
{
if (double.IsNaN(timestampSeconds) ||
double.IsInfinity(timestampSeconds) ||
timestampSeconds < 0d)
{
throw new ArgumentOutOfRangeException(
nameof(timestampSeconds),
timestampSeconds,
"The capture timestamp must be a finite, non-negative value.");
}
TimestampSeconds = timestampSeconds;
}
}
A study-specific subclass adds whatever domain fields the device needs,
and must forward the exact timestamp it received in
TryCaptureInput to the base constructor unchanged.
The examples below show how the shipped keyboard provider fills that contract.
Declaring the exact type.
This is why TOutputContract matters beyond your own
class: downstream, Command Mapping & Encoding declares the same
contract type and the Kernel resolves your provider against it at
startup. Pairing a provider with a downstream module that declares
a different contract type fails fast with a clear log
message — you never have to debug it by tracing runtime behaviour.
That resolution logic lives in the Kernel; you don't write or see
it, only its two possible outcomes.
Examples
The tabs show keyboard and analog joystick input providers.
A keyboard reports four discrete key states, so its contract is a set of booleans:
public sealed class ArrowInputPackage : InputProviderOutputContract
{
public bool Up { get; }
public bool Down { get; }
public bool Left { get; }
public bool Right { get; }
public float DeltaTime { get; }
public ArrowInputPackage(double timestamp, float dt, bool up, bool down,
bool left, bool right)
: base(timestamp)
{
DeltaTime = dt; Up = up; Down = down; Left = left; Right = right;
}
}
With the contract decided, the provider does exactly three things:
read the device, construct ArrowInputPackage, and
return true:
public sealed class KeyboardArrowProvider
: InputProvider<ArrowInputPackage>
{
[SerializeField] private int declaredPollRateHz = 50;
protected override bool TryCaptureInput(
double timestamp, out ArrowInputPackage output)
{
Keyboard keyboard = Keyboard.current;
if (keyboard == null)
{
output = null;
return false;
}
bool u = keyboard.upArrowKey.isPressed;
bool d = keyboard.downArrowKey.isPressed;
bool l = keyboard.leftArrowKey.isPressed;
bool r = keyboard.rightArrowKey.isPressed;
output = new ArrowInputPackage(timestamp, Time.deltaTime, u, d, l, r);
return true;
}
}
This study example reads an analog stick and publishes its value in a study-defined contract. Command Mapping & Encoding can then map the vector to the vehicle command it needs.
using Unite.Core;
using Unite.Kernel;
using UnityEngine;
using UnityEngine.InputSystem;
public sealed class JoystickInputPackage : InputProviderOutputContract
{
public Vector2 Stick { get; }
public JoystickInputPackage(double timestamp, Vector2 stick)
: base(timestamp)
{
Stick = stick;
}
}
public sealed class JoystickInputProvider
: InputProvider<JoystickInputPackage>
{
protected override bool TryCaptureInput(
double timestamp,
out JoystickInputPackage output)
{
Joystick joystick = Joystick.current;
if (joystick == null)
{
output = null;
return false;
}
output = new JoystickInputPackage(
timestamp,
joystick.stick.ReadValue());
return true;
}
}
Config
An Input Provider is a Unity component, so it's configured like the rest of a study: in the Inspector, not in code.
-
Add a concrete provider to the agent.
Add a component such as
KeyboardArrowProvideror the study example'sJoystickInputProviderto the same GameObject asTeleroboticsAgent. Every selected module must live on that one GameObject. - Assign it to the agent's Input Provider field. It's required — the agent refuses to start (logs an error and disables itself) if this field, Command Mapping & Encoding, Uplink Communication, or the Vehicle/Robot Model is left empty.
- Configure the provider's own settings, if it has any. The base class defines no configuration surface beyond the contract type — whatever a device needs (poll rate, key bindings, dead zones) is the concrete provider's own serialized fields.
The demo provider exposes one such field:
| Field | Type | Default | Description |
|---|---|---|---|
| Declared Poll Rate Hz | int |
50 |
Recorded alongside the study, not enforced — the Kernel always samples the provider at the agent's Update Rate. This field documents the device's real polling rate for a methods section. |
Same GameObject, one instance.
The agent's Awake validates that every selected
module — including the Input Provider — is attached to its own
GameObject; a reference to a component elsewhere in the scene
fails this check and disables the agent.