Step 2 - Build Gateway Subsystem
The cleaner robot tutorial provides a 2D arena simulator. The simulator is not the robot gateway that users should model. It is the environment that the robot runs against.
The R2Kernel-facing lower boundary is a simulator-backed gateway subsystem named
gateway.device_gateway.
gateway.device_gateway owns the runtime bridge to the provided simulator.
- drive commands for the mobile base
- arm extension commands
- digital I/O commands for gripper and disposal outputs
- standard rotating-radar and GNSS source Controls
It does not own simulator administration. Reset, trash spawn, and browser world state come from the provided simulator support API, not from the robot gateway.
The tutorial uses one compact gateway so the concept stays visible. A real robot could use one servo gateway for all motors, separate actuator and sensor gateways, or a vendor gateway that exposes several standard device API contracts.
The simulator process is separate.
tutorials/simulator/arena_server.py
It is not a R2Kernel subsystem. run_sim_world.py starts only the simulator and
browser. This step starts gateway.device_gateway as one managed package with
r2kernel launch run. Full deployment orchestration comes later, after the
platform and application exist.
Run This Stage
From the installed root.
cd <prefix>
r2kernel launch run tutorials/subsystems/gateway.device_gateway \
--instance gateway.device_gateway \
--set simulator.url=http://127.0.0.1:8092 \
--detach
Keep the simulator world from Step 1 running while this command starts. The gateway waits for the simulator API during startup because it is the robot-facing adapter over that already-running world.
Expected terminal output shape.
Launched package tutorial.robot_cleaner.device_gateway
mode: detached
instance: gateway.device_gateway
Open the browser with the same host rule from Step 1. For a local browser and runtime, use the following.
http://127.0.0.1:8091/
For a remote runtime host, use the following.
http://<runtime-host>:8091/
The robot still does not move from the browser. That is correct. This stage adds the gateway boundary only; there is no controller or application yet.
1. Open The Gateway Manifest
nano tutorials/subsystems/gateway.device_gateway/subsystem.yaml
The Gateway's public robot-facing interfaces are Control-only. The integrated rotating radar provides time-stamped sectors through a standard source Control; it does not provide detected targets. The GNSS source provides receiver-shaped antenna fixes through its standard source Control. Device-specific queries that the tutorial has not promoted to a standard Control API remain internal Component adapters.
subsystem:
name: gateway.device_gateway
role: gateway
provides:
actuator_servo:
contract: api.actuator.servo.v1
timed_digital_output:
contract: api.io.digital.timed_output.v1
digital_io:
contract: tutorial.gateway.device_gateway.digital_io.v1
visibility: internal
endpoints:
controls: [digital_output_set, digital_output_readback]
radar_source:
contract: api.r2kernel.sensor.radar.source.v1
gnss_source:
contract: api.r2kernel.sensor.gnss.source.v1
api.actuator.servo.v1 and api.io.digital.timed_output.v1 retain their
existing Control payloads and timing behavior. The simulator remains an
implementation detail behind both layers. A real gateway would translate the
public Control surfaces to EtherCAT, CAN, serial, a vendor SDK, or a simulator
API. The legacy digital-I/O adapters are internal tutorial wiring; radar and
GNSS use versioned common_control sample-exchange profiles. The GNSS payload
is an antenna position, not a body pose or heading; the Localizer converts it
to the configured map frame and combines it with odometry.
2. Read The Gateway Code
nano tutorials/subsystems/gateway.device_gateway/device_gateway.py
Focus on on_initialize().
self.register_control_endpoint("servo_joint", self._servo_joint)
self.register_control_endpoint("servo_link", self._servo_link)
self.register_control_endpoint("digital_output_command", self._control_digital_output)
self.register_control_endpoint("digital_output_set", self._set_digital_output)
self.register_control_endpoint("digital_output_readback", self._get_digital_output)
self.register_control_endpoint("radar_cycle", self._radar_cycle)
self.register_control_endpoint("gnss_cycle", self._gnss_cycle)
Register the complete declared surface before on_initialize() returns. The
SDK checks the manifest against these registrations before moving the Gateway
to Ready; with the default strict policy, a missing endpoint fails
initialization. Do not defer registration to on_start(). Ready is not
Operating: a Component or Gateway handler can receive a request before
on_start() finishes, so handler code must check lifecycle state and safely
reject commands until the subsystem is Operating.
The manifest and code must agree. The manifest says which contracts the gateway
claims to provide; the code registers the runtime endpoints required by those
contracts. Gateway public interfaces expose only Controls. The internal
digital_output_set/digital_output_readback adapters preserve the tutorial's
existing one-shot gripper behavior without exposing Procedures on the Gateway.
timed_digital_output supplies the separate digital_output_command Control
used for periodic, validity-bounded disposal output.
servo_joint is the actuator-servo Control surface from
api.actuator.servo.v1. It is intentionally registered only as a
Control endpoint. radar_cycle returns prepared sensor sectors with acquisition
times and range/bearing returns; it never returns simulator object IDs or
world-state coordinates. The perception Component alone turns those samples
into tracked application observations. Components keep their application-facing
Procedures and Events; they call or publish through their own contracts.
gnss_cycle returns the latest receiver-shaped sample with an immutable sample
identity. A receiver's no_fix state is distinct from the absence of a new
sample. It does not return simulator truth or a localizer pose.
The radar Control port has one explicit owner: the active deployment binding
to gateway.device_gateway.radar_source. The Gateway checks the current
runtime binding before serving a request and again before committing its
response. The first caller cannot claim the port. Missing, ambiguous, or
changed bindings cause the request to fail. The binding check validates runtime
topology, not an authenticated caller identity or a binding epoch; those are
separate runtime guarantees.
The 2D arena server and state logic live in the provided support modules.
nano tutorials/simulator/arena_server.py
nano tutorials/simulator/arena.py
You normally do not modify this simulator while learning R2Kernel. Treat it like a small IsaacSim, Gazebo, vendor SDK, or hardware environment that already exists.
3. Understand Simulator State
The browser reads simulator state directly from the provided simulator API. That API is tutorial infrastructure, not a robot gateway contract. It carries.
robot pose
trash list
scan result
score
world size
This gives the cleaner UI a single source of truth for drawing the arena.
4. Understand The Gateway Endpoints
Components never edit simulator state directly. They call robot-facing gateway endpoints.
| Endpoint | Called by | Effect |
|---|---|---|
servo_joint control |
timing-aware actuator clients | sends base or arm actuator commands through the servo gateway surface |
digital_output_command control |
incinerator component through timed_digital_output |
refreshes the disposal output with a validity lease and watchdog safe-off |
digital_output_set internal Control |
gripper component | preserves the tutorial's one-shot output-setting behavior |
digital_output_readback internal Control |
gripper component | reads the output state |
radar_cycle standard Control |
perception component | poll prepared, time-stamped sectors from the integrated rotating radar |
gnss_cycle standard Control |
localizer component | polls the latest antenna-frame WGS84 fix; the Localizer handles map conversion and odometry orientation |
Reset, spawn, and world-state sync are not listed because they belong to the provided simulator API. The world is visible, but it is not the robot-facing gateway contract.
5. Validate The Manifest Shape
Deployment validation will later validate this gateway as part of the whole robot. For now, read the file and check the registered endpoint names against the contracts.
rg -n "register_control_endpoint|register_procedure|register_event_publish" tutorials/subsystems/gateway.device_gateway/device_gateway.py
Only Control registrations should appear in the Gateway code. The manifest's internal endpoints and public API contracts must match those registrations.
Cleaner Robot Graph After This Step
The simulator can exist before the gateway starts. The R2Kernel gateway subsystem is the device adapter that exposes a Control-only robot-facing boundary. Components provide the application-facing Events, Procedures, and Operations.
Verify
Inspect this stage.
r2kernel subsystem status gateway.device_gateway
Then inspect the platform plan that will later assign the same gateway package to three robot slots.
r2kernel deploy plan tutorials/deployments/stages/robot.deployment.yaml
Look for the following.
gateway.device_gateway [gateway] slots=actuator_gateway:actuator_servo,gnss_sensor_gateway:gnss_source,radar_sensor_gateway:radar_source,timed_digital_output_gateway:timed_digital_output
Continue to Step 3 - Add Component Subsystems.