API Reference
This page is the reference for the messages exchanged between the participant client (agent) and the platform. It covers which messages flow in what order — from the registration handshake through mission delivery, answer submission, and scoring — and the specification of each message.
Message flow
The order in which the platform and the agent exchange messages, from the registration handshake through Stage 1/2 answer submission and scoring, is shown below. The detailed specification of each message is in the “ROS 2” section’s handshake and message dictionary.
sequenceDiagram
participant A as Participant agent
participant P as Platform
Note over A,P: Registration handshake (the token is never sent)
A->>P: SESSION_HELLO (100)
P->>A: SESSION_CHALLENGE (410)
A->>P: SESSION_PROOF (101)
P->>A: SESSION_ACK (400, issues session_key)
Note over A,P: Stage 1 — grounding (repeats for the number of selected problems)
loop each round
P->>A: MISSION_COMMAND (201)
A->>P: GROUNDING_RESULT (301)
P-->>A: TIME_REMAINING (203) / SCORE_RESULT (401)
end
P->>A: STAGE_TRANSITION (501)
Note over A,P: Stage 2 — grounding + driving to the target
P->>A: STAGE2_MISSION (211)
A->>P: STAGE2_GROUNDING_RESULT (311)
A->>P: cmd_vel (drive commands, repeated)
A->>P: TASK_COMPLETE (302)
P->>A: SCORE_RESULT (401)
Note over A,P: State COMPETITION_STATE (202): READY -> STAGE1_RUN -> STAGE2_RUN -> FINISHED
Implementation approaches
There are two ways to handle this message flow in your code.
marc_sdk (recommended) — the Python SDK wraps the ROS 2 communication, registration handshake, session, and QoS, and calls the callbacks you registered whenever an event occurs. You only implement the callbacks, without knowing the communication details. See the “marc_sdk” section below.
ROS 2 directly — exchange the topics and JSON messages above yourself, without the SDK. Use this when you need low-level control or are implementing in a language other than Python. See the “ROS 2” section below.
The basic usage of preparing the SDK and creating a client is explained first in the Technical Guide.
marc_sdk
At the center of marc_sdk is a single class, MARCClient. The ROS 2 communication with the
platform — the registration handshake, distinguishing message types, session and sequence
management, QoS, and CCTV camera auto-discovery — is handled internally by MARCClient.
Below is the full reference for its public API (source of truth: marc_sdk/client.py and
types.py).
Client creation and connection
MARCClient.from_env(**kwargs) -> MARCClientParameters: reads the team ID and authentication token from the environment variables
MARC_TEAM_ID/MARC_TOKEN. Additional keyword arguments are passed through toMARCClient(...).Returns: an initialized
MARCClient. Instead of environment variables, you can also create it directly withMARCClient(team_id=..., token=...).
connect(timeout=30.0, register_period=2.0) -> booltimeout(float, default30.0) — the maximum time (seconds) to wait forSESSION_ACKregister_period(float, default2.0) — the registration retry period (seconds)Behavior: rclpy init -> create node/publishers/subscribers -> challenge-response handshake (HELLO -> CHALLENGE -> PROOF -> ACK) -> start the background executor.
Returns:
Trueif authentication succeeds.
run()Parameters: none
Behavior: spins the node and blocks the main thread (exit with Ctrl-C). Callbacks are called on the background executor thread.
shutdown()Parameters: none
Behavior: sends a stop command, then cleans up the node/executor (safe to call more than once).
is_running(property,bool)Trueduring the Stage 2 driving phase. Use it as the exit condition for theon_stage2_runloop.
stage2_reveal(property)The most recent Stage 2 reveal (
Stage2RevealorNone). Use it when checking by polling instead ofon_stage2_reveal.
Callbacks
While the platform and the SDK communicate, moments arise when the agent should take some action (e.g. a new mission arriving) or when real-time information must be delivered to the agent (e.g. time remaining, state changes, scoring results). If you register a callback function on the decorator that corresponds to each event, the SDK automatically calls the registered callback when that event occurs. Callbacks are called on the background executor thread.
Decorator |
Summary |
|---|---|
|
Stage 1 round problem issued |
|
Stage 2 problem issued |
|
Stage 2 driving begins |
|
Stage 2 grounding score revealed |
|
competition state transition |
|
time-remaining update |
|
time limit expired |
|
stage transition |
|
scoring result |
|
warning |
Each callback is organized as (when it fires / signature / what to do inside the callback) below. The signature is the arguments the registered callback function receives, and the sub-items are the type and meaning of each argument (or its fields).
Mission handling
on_missionFires: when the platform issues each Stage 1 round problem (msg 201)
Signature:
fn(mission)mission.voice_command(str) — the person’s natural-language command to processmission.round(int) — the current round numbermission.total_rounds(int) — the total number of Stage 1 roundsmission.time_limit(float) — this round’s time limit (seconds)
Implement: interpret the natural-language command, find the target, and submit with
submit_grounding()
on_stage2_missionFires: when the platform issues the Stage 2 problem (msg 211)
Signature:
fn(mission)mission.task_description(str) — the task command to processmission.time_limit(float) — the time limit (seconds)mission.owner_position(list[float]orNone) — the position[x, y, z]to bring the object to (Noneif there is no owner_zone)
Implement: interpret the command, find the target, and submit with
submit_stage2_grounding()
on_stage2_revealFires: right after Stage 2 grounding is scored (msg 411)
Signature:
fn(reveal)reveal.grounding_score(float) — the score of this grounding submission (0–100)reveal.target_type(str) — the type of object to pickreveal.hint_center(list[float]) — the center of the target’s approximate location[x, y, z](not the exact answer)reveal.hint_radius(float) — the approximate-location radius (m)
Implement: from the candidates within the radius, choose the object by
reveal.target_type, and prepare to collect toward this location (can also be polled via theclient.stage2_revealproperty)Note: this is designed so that even if you submitted the Stage 2 grounding incorrectly, the platform tells you the approximate answer location so you can continue navigation and pick & place. Thanks to this, your collection-and-delivery ability can be evaluated regardless of grounding accuracy.
on_stage2_runFires: when entering the Stage 2 driving phase (a separate thread)
Signature:
fn()— no argumentsImplement: loop while
client.is_runningis true, doing navigation, picking, and delivery, and calltask_complete()when done
State and time notifications
on_state_changeFires: when the competition state changes (msg 202)
Signature:
fn(old, new)old(str) — the previous statenew(str) — the next state (READY->STAGE1_RUN->STAGE2_RUN->FINISHING->FINISHED)
Implement: initialize/transition internal logic per state
on_transitionFires: on a stage transition notice (msg 501)
Signature:
fn(from_state, to_state)from_state(str) — the state before the transitionto_state(str) — the state after the transition
Implement: handle the transition point (similar to
on_state_change)
on_time_remainingFires: when the time remaining is updated periodically (msg 203)
Signature:
fn(remaining)remaining(float) — the time remaining (seconds)
Implement: adjust strategy by time (e.g. hurry when little time is left)
on_time_expiredFires: when the time limit ends (msg 204)
Signature:
fn(which)which(str) — the kind of time that ended ('stage1'/'total')
Implement: clean up / stop the work in progress
Results and warnings
on_scoreFires: when a scoring result arrives (msg 401)
Signature:
fn(score)score.total(float) — the total scorescore.scores(dict) — the detailed score itemsscore.is_final(bool) — whether it is the final result (Falsefor a round score)score.round(intorNone) — the round number (Nonefor the final result)
Implement: check / record the score
on_warningFires: when the platform sends a warning (msg 502)
Signature:
fn(type, message)type(str) — the warning typemessage(str) — the warning message
Implement: record or respond to the warning type and message
Sensor queries
The sensor data the platform publishes is queried with the functions below. Every function
returns the most recently received ROS 2 message as-is (thread-safe), and returns None if
no value has been received yet.
Function |
Summary |
|---|---|
|
list of available CCTV ids |
|
CCTV RGB image |
|
CCTV intrinsics |
|
robot camera RGB image |
|
robot camera depth image |
|
robot lidar scan |
|
robot odometry |
|
robot IMU |
|
robot arm joint state |
|
whether the gripper is holding an object |
|
whether an object is loaded in the basket |
|
occupancy grid map |
|
robot position and orientation |
|
subscribe to an arbitrary topic directly |
list_cctv() -> list[str]Parameters: none
Returns: the list of discovered CCTV camera ids. Pass these ids to the two functions below.
get_cctv_image(camera_id: str) -> sensor_msgs/Image | Nonecamera_id(str) — the CCTV id to query (check withlist_cctv())Returns: that camera’s latest RGB frame (encoding
rgb8). The primary input for VLA perception.
get_cctv_info(camera_id: str) -> sensor_msgs/CameraInfo | Nonecamera_id(str) — the CCTV id to queryReturns: that camera’s intrinsics (projection matrix K, etc.). Used for pixel<->3D conversion.
get_robot_image(which: str = "base_left") -> sensor_msgs/Image | Nonewhich(str, default"base_left") — the robot camera position. One ofbase_left,base_right,gripper_left,gripper_right(other values raiseValueError).Returns: that camera’s latest RGB image (
rgb8).
get_robot_depth(which: str = "base") -> sensor_msgs/Image | Nonewhich(str, default"base") — the depth camera family. One ofbase,gripper(other values raiseValueError).Returns: that camera’s depth image (
32FC1, pixel value = distance in metres).
get_lidar() -> sensor_msgs/LaserScan | NoneParameters: none
Returns: the robot’s 2D lidar scan. Used for close-range obstacle avoidance.
get_odom() -> nav_msgs/Odometry | NoneParameters: none
Returns: robot odometry (position and velocity).
get_imu() -> sensor_msgs/Imu | NoneParameters: none
Returns: inertial sensor values (orientation, angular velocity, acceleration).
get_arm_state() -> sensor_msgs/JointState | NoneParameters: none
Returns: the robot arm’s current joint state (joint angles, etc.).
is_grasping() -> bool | NoneParameters: none
Returns:
Trueif the gripper is currently holding an object, elseFalse(Nonebefore the first message arrives). Isomorphic to the object detection of a real Robotiq 2F-140: if a close command is blocked by an object and cannot close to the target angle, it isTrue. Used to detect a failed pick (staysFalse) or a drop in transit (True->False) and retry.
is_basket_occupied() -> bool | NoneParameters: none
Returns:
Trueif an object is currently in the rear basket, elseFalse(Nonebefore the first message arrives). Like a real basket-load sensor, it is a generic signal that only tells you “whether something is loaded”; it does not tell you whether the correct object was delivered or the score. Used to confirm delivery success or detect a case where it fell outside the basket (retry). The provided cameras cannot see the rear basket, so confirm delivery with this signal. Published in Stage 2 only.
get_occupancy_map() -> nav_msgs/OccupancyGrid | NoneParameters: none
Returns: the static 2D occupancy grid (drivable road only). The input for path planning.
get_world_pose() -> tuple[float, float, float] | NoneParameters: none
Returns: the robot’s world coordinate and orientation
(x, y, yaw_rad). Same coordinate frame as the submitted coordinatetarget_coord, and the position feedback for Stage 2 driving.
subscribe(topic: str, msg_type, callback, qos=None)topic(str) — the ROS 2 topic name to subscribe tomsg_type— the message type class (e.g.sensor_msgs.msg.Image)callback— the functionfn(msg)to call when a message is receivedqos(defaultNone) — the QoS profile. Default (RELIABLE) ifNoneReturns: the rclpy subscription object (
rclpy.subscription.Subscription)Note: an escape hatch to subscribe to an arbitrary topic directly when the functions above are not enough.
Robot control
Controls the robot body’s motion and the robot arm.
Function |
Summary |
|---|---|
|
body motion command |
|
stop the body |
|
robot arm joint command |
send_cmd_vel(linear_x=0.0, linear_y=0.0, angular_z=0.0, twist=None)linear_x(float, default0.0) — forward/back speed (m/s, + is forward)linear_y(float, default0.0) — left/right lateral speed (m/s, + is left)angular_z(float, default0.0) — rotation speed (rad/s, + is counter-clockwise)twist(geometry_msgs/Twist, defaultNone) — if given, this Twist is published as-is and the three arguments above are ignoredReturns: none
Note: max allowed 1.5 m/s / 1.5 rad/s. The SDK does not automatically clamp excess input, so the agent must respect it.
stop()Parameters: none
Behavior: stops the body immediately (publishes
cmd_vel0)Returns: none
send_arm_command(joint_state)joint_state(sensor_msgs/JointState) — the robot arm’s target joint state (6-axis joint angles)Behavior: publishes as-is to the robot arm joint command topic (passthrough)
Returns: none
Answer submission
Submits the Stage 1/2 answers and notifies Stage 2 collection completion. A grounding answer
is passed only as the GroundingResult type (see “Data types” below).
Function |
Summary |
|---|---|
|
submit Stage 1 grounding (msg 301) |
|
submit Stage 2 interpretation (msg 311) |
|
Stage 2 collection done (msg 302) |
submit_grounding(result: GroundingResult) -> intresult(GroundingResult) — the grounding result to submit. Accepts onlyGroundingResult. If you already have a payload dict, convert it withGroundingResult.from_payload(payload)and pass that.Behavior: submit the Stage 1 grounding answer (msg 301)
Returns: the assigned request sequence number
seq(int)
submit_stage2_grounding(result: GroundingResult) -> intParameters: same as
submit_grounding(GroundingResult)Behavior: submit the Stage 2 interpretation answer (msg 311)
Returns:
seq(int)
task_complete() -> intParameters: none
Behavior: publish Stage 2 collection completion (msg 302). It switches to the finishing phase the moment it is called and cannot be cancelled.
Returns:
seq(int)
Data types
The data types exchanged as callback arguments and submission results (source of truth:
marc_sdk/types.py). Import with from marc_sdk import GroundingResult.
GroundingResult— the grounding result you build and submit.camera_id(str) — the CCTV id where the target is visibletarget_type(str) — the target’s type ("person"for person problems)anchor_coord(list[float]) — the estimated reference-landmark coordinate[x, y, z]target_coord(list[float]) — the estimated target coordinate[x, y, z]landmark(str, default"") — the reference landmark name (when there is a spatial relation)relation(str, defaultNone) — the spatial relation (used in lost-item relation problems)situation(str, defaultNone) — the situation category (used in SAR person problems)Methods:
GroundingResult.from_payload(payload: dict) -> GroundingResult(create from a payload dict),to_payload() -> dict(serialize to the submission payload)
Mission— theon_missionargument. Fields:voice_command(str),round(int),total_rounds(int),time_limit(float).Stage2Mission— theon_stage2_missionargument. Fields:task_description(str),time_limit(float),owner_position(list[float]orNone).Stage2Reveal— theon_stage2_revealargument. Fields:grounding_score(float),target_type(str),hint_center(list[float]),hint_radius(float).Score— theon_scoreargument. Fields:total(float),scores(dict),is_final(bool),round(intorNone),stage(str).
ROS 2
The platform and the agent communicate entirely over ROS 2. Since marc_sdk wraps this
layer, you normally do not deal with it directly, but reference this section when accessing at
a low level or checking a specification. There are two transport families.
Operations (
/marc/ops/...) — astd_msgs/Stringcarrying JSON with aheader/payloadstructure and a numericmsgid. Session, mission, submission, scoring.Sensors / control — standard ROS 2 messages on dedicated topics (Image, CameraInfo, Twist, JointState, LaserScan, OccupancyGrid, TF).
Namespaces
/marc/
├── ops/ # ops (JSON + msg id)
│ ├── register # session handshake (fixed topic)
│ ├── announce # Platform -> All (mission / state / time)
│ └── {team_id}/
│ ├── request # Participant -> Platform
│ ├── response # Platform -> Participant
│ └── notification # Platform -> Participant (async)
├── env/ # env (ROS 2 standard)
│ ├── cctv/{id}/image, info # CCTV RGB + camera params
│ └── map/occupancy, metadata # static 2D occupancy grid
├── {team_id}/robot/ # per-team robot sensors / control
│ ├── base_camera/{left,right}/image, info, depth/image, points
│ ├── gripper_camera/{left,right}/image, info, depth/image, points
│ ├── odom, imu, lidar/scan
│ ├── arm/joint_states, arm/joint_command
│ ├── gripper/holding # grasp-hold feedback (std_msgs/Bool)
│ ├── basket/occupied # basket presence, Stage 2 (std_msgs/Bool)
│ └── cmd_vel
/tf
/clock
Handshake (session authentication)
Registration is a challenge-response handshake. The long-term secret, the team token, is
never sent on the wire; you prove possession with an HMAC and receive an expiring
session_key.
HELLO (100) -> CHALLENGE (410) -> PROOF (101) -> SESSION_ACK (400)
participant platform participant platform
HELLO (msg 100)
/marc/ops/register—{ team_id, team, client_nonce }.CHALLENGE (msg 410)
.../response—{ server_nonce, ttl, alg: "HMAC-SHA256" }.PROOF (msg 101)
/marc/ops/register—proof = HMAC-SHA256(token, "server_nonce|client_nonce|team_id").SESSION_ACK (msg 400)
.../response—{ status: "ok", session_key, expires_at }.
After ACK, every request message carries the issued session_key in header.session. The
token value and how it is derived are not published here — you receive your token out of
band. The SDK performs the whole handshake for you in connect().
Message header
Every operations message has the structure { "header": {...}, "payload": {...} }.
Field |
Type |
Required on |
Description |
|---|---|---|---|
|
int |
all |
Message type id |
|
float |
all |
Publish time (Unix epoch, seconds) |
|
int |
request, response |
Request/response pairing key; monotonically increasing per session (replay defense) |
|
string |
request only |
The issued |
Control topics
cmd_vel—geometry_msgs/TwistTopic:
/marc/{team_id}/robot/cmd_vellinear.x(float, m/s) — forward/back (+ = forward)linear.y(float, m/s) — lateral (+ = left)angular.z(float, rad/s) — yaw (+ = counter-clockwise)Constraints: motion plane XY (2D), frame Z-up right-handed (REP-103), max linear speed 1.5 m/s / max angular speed 1.5 rad/s
Note:
linear.z,angular.x,angular.yare unused.
arm/joint_command—sensor_msgs/JointStateTopic:
/marc/{team_id}/robot/arm/joint_commandDoF: 6 (
joint_1…joint_6), control mode = position target (radians)State publish: on
arm/joint_states, per physics step (nominal 20 Hz). The actual delivery rate can drop depending on how fast the run machine keeps up with the simulation relative to real time (its real-time factor) - when slower, state arrives less often - so advance your control loop on the arrival of this state feedback rather than on a fixed period.Note: a 6-DoF (non-redundant) manipulator + Robotiq 2F-140 gripper; reach comes from base + arm coordination. See the Technical Guide -> Manipulation learning kit.
gripper/holding—std_msgs/Bool(published by the platform)Topic:
/marc/{team_id}/robot/gripper/holdingMeaning:
trueif the gripper is holding an object. Isomorphic to the object detection of a real Robotiq 2F-140: if a close command is blocked by an object and cannot close to the target angle, it istrue. Used to detect a failed pick or a drop in transit and retry (SDKis_grasping()).
basket/occupied—std_msgs/Bool(published by the platform, Stage 2)Topic:
/marc/{team_id}/robot/basket/occupiedMeaning:
trueif an object is currently in the rear basket. Like a real basket-load sensor, it is a generic signal that only tells you “whether something is loaded” and does not expose correctness or score. Used to confirm delivery success and retry (SDKis_basket_occupied()).
Sensor topics
CCTV (environment)
/marc/env/cctv/{id}/image—sensor_msgs/Image, RGB (rgb8)/marc/env/cctv/{id}/info—sensor_msgs/CameraInfo, intrinsics (K)Properties: resolution 1280 x 720 (HD, 16:9),
frame_id={camera_id}Note: discover available ids by listing topics under
/marc/env/cctv/.
Robot stereo cameras
There is a stereo camera on each of the base (
base_camera/, driving + search) and the gripper (gripper_camera/, close-range pick-and-place).Each camera:
left/rightRGB (rgb8),depth/image(32FC1, m),points(sensor_msgs/PointCloud2)
Map
/marc/env/map/occupancy—nav_msgs/OccupancyGrid, static 2D grid, latched/marc/env/map/metadata—nav_msgs/MapMetaData, resolution, origin, sizeNote: the occupancy map contains drivable road only. People and landmarks are not in the map (detect them with sensors); frame
map, Z-up, metres.
Other robot sensors (all under /marc/{team_id}/robot/):
Topic |
Type |
Rate (nominal) |
|---|---|---|
|
|
20 Hz |
|
|
20 Hz |
|
|
— |
|
|
20 Hz |
|
|
20 Hz |
|
|
20 Hz (Stage 2) |
Note
The rates in the table are the nominal physics-step rate (20 Hz). State topics are published every simulation step, so if the run machine cannot keep up with real time (e.g. with the GUI on or low GPU performance) the actual delivery rate can be lower. Write your control and decision logic to act as messages arrive, not assuming a fixed rate.
Note
The exact lidar range/FOV/rate and the stereo resolution/baseline are published with the frozen release once the robot USD asset is finalized.
TF / coordinate frames
Item |
Spec |
|---|---|
Standard |
ROS 2 REP-103 |
Axes |
X = forward, Y = left, Z = up (right-handed) |
Units |
metres; radians (Twist), degrees (YAML) |
TF |
|
Clock |
|
Isaac Sim 5.x is Z-up right-handed, so no axis conversion is needed.
Mission-area ground-plane assumption
Mission areas are computed by projecting each camera ray onto a per-camera ground plane
(z = ground_height). They are not treated as one global plane over the whole background —
there is roughly a 10 cm step between road and grass, so account for the relevant ground
height when back-projecting CCTV pixels to world coordinates.
QoS profiles
Topic class |
Reliability |
Durability |
History |
Depth |
|---|---|---|---|---|
Operations (JSON), |
RELIABLE |
VOLATILE |
KEEP_LAST |
10 |
|
RELIABLE |
TRANSIENT_LOCAL |
KEEP_LAST |
10 |
Sensor images |
BEST_EFFORT |
VOLATILE |
KEEP_LAST |
1 |
TRANSIENT_LOCAL lets a late joiner still receive the SESSION_ACK and the latched map.
Message dictionary
id |
Name |
Topic |
Direction |
|---|---|---|---|
100 |
SESSION_HELLO |
|
Participant -> Platform |
101 |
SESSION_PROOF |
|
Participant -> Platform |
201 |
MISSION_COMMAND (Stage 1) |
|
Platform -> All |
202 |
COMPETITION_STATE |
|
Platform -> All |
203 |
TIME_REMAINING |
|
Platform -> All |
204 |
TIME_EXPIRED |
|
Platform -> All |
211 |
STAGE2_MISSION |
|
Platform -> All |
301 |
GROUNDING_RESULT (Stage 1) |
|
Participant -> Platform |
302 |
TASK_COMPLETE (Stage 2) |
|
Participant -> Platform |
311 |
STAGE2_GROUNDING_RESULT |
|
Participant -> Platform |
400 |
SESSION_ACK |
|
Platform -> Participant |
401 |
SCORE_RESULT |
|
Platform -> Participant |
410 |
SESSION_CHALLENGE |
|
Platform -> Participant |
501 |
STAGE_TRANSITION |
|
Platform -> Participant |
502 |
WARNING |
|
Platform -> Participant |
Competition states (msg 202): READY -> STAGE1_RUN -> STAGE2_RUN -> FINISHING ->
FINISHED.
Grounding payload (msg 301 / 311)
Single-target model. target_type is a single string; the coordinates are a single 3D point.
With the SDK, the GroundingResult (see “Data types” above) is serialized into this payload.
{
"camera_id": "rig_1_a",
"interpretation": {
"target_type": "cola_can",
"landmark": "bench",
"relation": "beside",
"situation": "normal"
},
"grounding": {
"anchor_coord": [-62.3, 151.2, 16.5],
"target_coord": [-62.1, 151.5, 16.5]
}
}
Field |
Meaning |
|---|---|
|
Single object type. Person problems use |
|
Reference landmark name (when a relation is involved) |
|
Spatial relation ( |
|
Situation category ( |
|
Estimated landmark coordinate |
|
Estimated target coordinate |