The shape of a program
The SDK never calls your code: your program does its setup and then owns the loop.
Two shapes, and this SDK is the second one
Robotics clients come in two shapes. In the first, a framework owns the program: you
subclass something, fill in setup() and update(), hand the class to a runner, and
the runner calls you. In the second, you own the program: main() does its setup and
then runs a plain loop, calling the library when it wants something.
The first is the Arduino shape. This SDK is deliberately the second, the STM32 shape.
flowchart TB
subgraph AR["Arduino shape: the framework calls you"]
direction TB
A1["runner starts"] --> A2["your setup"]
A2 --> A3["your update callback"]
A3 --> A4["runner decides when"]
A4 --> A3
end
subgraph ST["STM32 shape: you call the SDK"]
direction TB
B1["main starts"] --> B2["connect"]
B2 --> B3["read states"]
B3 --> B4["your control law"]
B4 --> B5["send command"]
B5 --> B6["rate or wait_new_state"]
B6 --> B3
end
There is no base class, no runner, no setup()/update() pair, and no registration
call. The SDK never calls your code. Every arrow in the right-hand diagram is a
call your main makes.
The canonical program
This is the whole shape, and every example in the book is a variation on it, quoted from each surface’s own documentation:
The crate documentation in crates/vrobots-sdk/src/lib.rs:
use vrobots_sdk::{RobotType, VirtualRobot, VrError};
fn main() -> Result<(), VrError> {
// ===== setup =====
vrobots_sdk::init_logging("info");
// `Some(sys_id)` attaches to a robot; `None` creates a new one.
let robot = VirtualRobot::connect(RobotType::Multirotor, Some(1))?;
// ===== loop =====
loop {
let s = robot.states(); // latest snapshot, never torn
let [x, y, z] = s.kin.lin_pos;
println!("t={:.3} pos=({x:.2},{y:.2},{z:.2})", s.elapsed);
robot.set_mr_pwm([1501.0; 4])?; // your controller's output
robot.rate(100.0); // drift-compensated pacing, Hz
}
}
Each surface documents this same shape as its own opening example, which is the clearest
evidence that the shape is the API rather than a Rust convention. The one structural
difference is that C++ and Python split construction from connection into two statements,
where Rust’s connect is a constructor that does both.
The docstring’s program is a flat script, and so is every Python example: the top level of
the file does the setup and then runs the loop, so the script itself plays the part of main
and nothing else about the shape changes. examples/python/ex01_hello_states.py is the
docstring’s program at 50 Hz and without the PWM command.
There is no expected output to quote: the loop never terminates, printing one line
per iteration until you stop it with Ctrl-C. examples/rust/src/bin/ex01_hello_states.rs
is this program with the printing worked out, and it is the subject of
Hello states.
Read the four calls in the loop as four separate decisions you are making:
| Call | What it is | What it is not |
|---|---|---|
states() | a read of the latest snapshot | a request to the simulator |
| your control law | the whole point of the program | anything the SDK participates in |
set_mr_pwm(..) | a publish, returning when the bytes are queued | a round trip with a result |
rate(100.0) | drift-compensated sleep, a helper | a scheduler that owns your timing |
Why this shape
Three reasons, in the order they matter.
A control loop has a rate, and it is yours. A callback framework decides when your
code runs. A cascaded controller with an inner loop at 200 Hz and an outer at 20 Hz
does not fit that, and neither does a program that wants to run as fast as samples
arrive. Owning the loop means the rate is a line of your code:
rate for a fixed schedule,
wait_new_state for one iteration per published
sample.
It is the shape the target hardware has. Code written against this SDK is meant
to move to a real vehicle, where main is a while (1) over a timer. Keeping the
simulator client the same shape means the loop body ports; a callback body does not.
There is nothing to learn. No lifecycle, no ordering rules between setup and the
first update, no question about which thread a callback runs on. The SDK does own
background threads (one subscriber per state stream, one reader per camera stream),
and none of them ever enters your code. They keep a snapshot fresh; you read it when
you like.
Note. One process is not limited to one robot. A
VirtualRobotis a handle, so construct as many as you need, each with its own session and snapshot. Callrate()on exactly one of them, or you sleep once per handle per iteration.examples/rust/src/bin/ex18_multi_robot.rsis the worked case.
Dropping the handle does not stop the robot
Because main owns the program, it is tempting to read the end of main, or the end of
a Python script, as the end of everything. It is not. Dropping a VirtualRobot closes the
zenoh session, unsubscribes, and stops the SDK’s own threads. It leaves the robot
running, still latched on the last command it received, until something explicitly
deletes it or the scene is reloaded.
That is the intended behaviour rather than an oversight, and it is
rule four. If your program should leave nothing behind, call
delete() before it ends, which is what the examples that create a robot do.
Next: What connect actually does
See also: Pacing your loop, More than one robot, Robot lifecycle