How Mobility Works
This guide explains how mobility works in the prototype-mode simulator, what a mobility handler does, how mobility commands are interpreted, and how to switch between the available mobility models.
Use this page for the conceptual model. For the API reference, see:
The mobility flow
Mobility in the simulator is split between the protocol and the active mobility handler:
- Your protocol decides that the node should move.
- It sends a
MobilityCommandthroughself.provider.send_mobility_command(...). - The provider forwards that command to the handler registered under the label
"mobility". - The selected mobility handler interprets the command and updates the node's mobility state on each simulation tick.
- The handler emits telemetry back to the protocol through
handle_telemetry(...).
This separation is intentional. The protocol decides what it wants. The handler decides what that command means in the current mobility model.
What a handler is
A handler is a simulator component that is injected into the event loop. It can interact with the event loop by scheduling events and has access to node instances.
Mobility handlers are responsible for:
- receiving mobility commands from protocols
- updating positions over time
- optionally keeping extra internal state, such as targets or current velocity
- sending telemetry back to protocols
In practice, the mobility handler is the simulator-side implementation of movement. The same protocol API stays stable, while different handlers can model different kinds of motion.
Why the mobility message is generic
MobilityCommand is a
generic message with a command type plus up to six numeric parameters.
That means the command object itself does not fully define the behavior. The meaning comes from two things together:
- the
command_type - the handler that receives it
This is why the project provides typed helpers such as:
GotoCoordsMobilityCommandGotoGeoCoordsMobilityCommandSetSpeedMobilityCommandSetVelocityMobilityCommand
These wrappers make protocol code clearer, but they still serialize into the
same generic MobilityCommand structure.
How the existing handlers interpret commands
The simulator currently exposes two mobility handler families.
Massless mobility
MobilityHandler is the backwards-compatible alias for
MasslessMobilityHandler.
This model treats nodes as points with no inertia:
GOTO_COORDSsets a target point in Cartesian coordinatesGOTO_GEO_COORDSconverts geographic coordinates and sets a target pointSET_SPEEDsets the scalar travel speed in meters per second- movement toward the target is updated every
update_rate - telemetry reports position using the base
Telemetrytype
This handler is useful when you want simple target-based movement and do not need acceleration or velocity-state dynamics.
Dynamic velocity mobility
DynamicVelocityMobilityHandler
uses a different interpretation.
This model does not move toward waypoints. Instead:
SET_SPEEDis interpreted as a desired velocity vectorparam_1,param_2, andparam_3are treated asvx,vy, andvz- the handler tracks that desired velocity subject to acceleration and speed limits
- position is integrated from the current velocity on each update
- telemetry reports both position and velocity through
DynamicVelocityTelemetry
In protocol code, the clearest way to express this is
SetVelocityMobilityCommand:
from gradysim.protocol.messages.mobility import SetVelocityMobilityCommand
command = SetVelocityMobilityCommand(3.0, 0.0, 1.5)
self.provider.send_mobility_command(command)
The important detail is that the message is still generic internally. The
dynamic velocity handler gives SET_SPEED a velocity-vector meaning.
How telemetry changes with the handler
Protocols always receive mobility state through
handle_telemetry(...).
What changes is the telemetry payload.
With the massless handler:
- telemetry contains current position
With the dynamic velocity handler:
- telemetry contains current position
- telemetry also contains current velocity
Example:
from gradysim.protocol.messages.telemetry import Telemetry
from gradysim.simulator.handler.mobility.dynamic_velocity.telemetry import DynamicVelocityTelemetry
def handle_telemetry(self, telemetry: Telemetry) -> None:
position = telemetry.current_position
if isinstance(telemetry, DynamicVelocityTelemetry):
velocity = telemetry.current_velocity
How to switch handlers
Switching handlers is done when building the simulation. There is no separate
runtime toggle. The active mobility model is whichever handler you register with
the label "mobility" through
SimulationBuilder.add_handler(...).
Using the default massless mobility handler
from gradysim.simulator.handler.mobility import MobilityConfiguration, MobilityHandler
builder.add_handler(
MobilityHandler(
MobilityConfiguration(
update_rate=0.05,
default_speed=10.0,
)
)
)
This is the same model already used by most existing examples and plugins.
Switching to the dynamic velocity handler
from gradysim.simulator.handler.mobility import (
DynamicVelocityMobilityConfiguration,
DynamicVelocityMobilityHandler,
)
builder.add_handler(
DynamicVelocityMobilityHandler(
DynamicVelocityMobilityConfiguration(
update_rate=0.05,
max_speed_xy=10.0,
max_speed_z=5.0,
max_acc_xy=4.0,
max_acc_z=4.0,
tau_xy=0.5,
tau_z=0.8,
)
)
)
After this change, the protocol still calls
self.provider.send_mobility_command(...), but the command should now match the
dynamic velocity semantics.
Choosing the right handler
Use the massless handler when:
- your protocol thinks in destinations or waypoints
- you want simple movement with minimal configuration
- you are using plugins built around target-based movement
Use the dynamic velocity handler when:
- your controller outputs velocity vectors directly
- you need acceleration limits or first-order velocity tracking
- your protocol needs current velocity in telemetry
Practical rule
Write protocol code against the provider interface, but choose mobility command helpers that match the handler you registered.
That is the main contract:
- the provider API is stable
- the message container is generic
- the handler defines the actual motion model