Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello states

Your first program: connect to a robot and print its position at your own rate.

cargo run -p vrobots-examples --bin ex01_hello_states
./target/cpp-build/ex01_hello_states
python examples/python/ex01_hello_states.py

The whole program

There is no framework here. The program does its setup, then owns a plain infinite loop: inside main in Rust and C++, and at the top level of the script in Python.

examples/rust/src/bin/ex01_hello_states.rs:

use vrobots_sdk::{RobotType, VirtualRobot, VrError};

const SYS_ID: u32 = 1; // the multirotor in the test scene
const HZ: f64 = 50.0;

fn main() -> Result<(), VrError> {
    // ===== setup =====
    vrobots_sdk::init_logging("info");
    let robot = VirtualRobot::connect(RobotType::Multirotor, Some(SYS_ID))?;

    // ===== loop =====
    loop {
        let s = robot.states(); // immutable latest snapshot, never torn
        let [x, y, z] = s.kin.lin_pos;
        println!("State t={:.3} pos=({x:.3},{y:.2},{z:.2})", s.elapsed);
        robot.rate(HZ); // drift-compensated pacing, Hz
    }
}

Three surfaces, one shape: setup, then a loop you own. Rust calls init_logging, the C++ version registers a log callback, and the Python script sets up no logging of its own. states is a method in Rust and C++ but a property in Python. On a failure, Rust returns the error from main, C++ catches it and prints its code, and the Python script ends with the VrError traceback. Fifty lines a second, until you press Ctrl-C:

State t=0.000 pos=(0.000,1.05,0.00)
State t=0.020 pos=(0.000,1.05,0.00)
State t=0.040 pos=(0.000,1.05,0.00)
State t=0.060 pos=(0.000,1.05,0.00)

In Rust, init_logging("info") also prints the SDK’s own connect progress to standard output. The Python package forwards the SDK’s warnings to Python logging as soon as vrsdk is imported, so the script shows those and nothing quieter; vrsdk.init_logging("info") turns the connect progress on. Your position numbers will differ; a robot sitting still on the ground is the expected first sight.

The STM32 shape, line by line

Read the program as four moves. The Python script makes the last three.

  1. init_logging("info") registers a handler that prints the SDK’s log events. RUST_LOG overrides the filter you pass, and the call is a no-op if a handler is already registered.
  2. VirtualRobot::connect(RobotType::Multirotor, Some(SYS_ID)) attaches to a robot the scene already contains. Some(id) never touches the manager’s create service. It subscribes the state topic and blocks until the first snapshot arrives, so states() is valid the instant connect returns.
  3. robot.states() hands back the latest snapshot. This is the whole read API.
  4. robot.rate(HZ) sleeps until the next tick, compensating for drift, so the loop runs at the rate you asked for rather than that rate minus your own work.

There is no base class, no runner, no setup() and no update() callback. The SDK never calls your code. This is the single biggest difference from the older Python client, and The shape of a program is the page that argues it.

What states() guarantees

Three guarantees, and one absence that surprises people.

  • It never blocks. An SDK-owned subscriber thread keeps a snapshot fresh; you read it at whatever rate suits your controller. A 50 Hz loop against a 25 Hz stream is legal and sees each sample twice.
  • It is never torn. You get a whole snapshot or the previous whole snapshot, never a position from one sample and a velocity from the next.
  • It never fails. There is no error return. If the simulator stops, states() keeps handing back the last snapshot forever, so a frozen robot and a stopped simulator look identical from inside the loop.
  • There is no “is it new” flag. Nothing on the snapshot says whether you have already seen it.

Gotcha. Because a stall presents as unchanging numbers rather than an error, polling states() cannot detect one. wait_new_state(timeout) blocks for a genuinely newer sample and returns VrError::Timeout when none arrives, which is a status rather than a failure. That is the detector, and ex09_state_paced_loop is the example.

elapsed is not a wall clock

s.elapsed is seconds since this robot’s first state sample, as an f64, monotonic, and shared by every one of that robot’s streams including its cameras. It is the field to print and to plot against.

It is not the time of day and it is not your process’s uptime. When you need to compare across streams, use s.t_ns, which is nanoseconds since the unix epoch on the simulator’s clock and is directly subtractable from a camera frame’s t_ns. s.seq is the per-topic sequence number: a jump larger than one means a sample was dropped.

Which id is the multirotor

The id is 1, SYS_ID in Rust and C++ and sys_id=1 in the Python call, because in the test scene, booted straight into the Flatworld scene, sys_id 1 is the multirotor and sys_id 0 is the truck.

That is a convenience, not a contract. Ids are allocated at scene load and keep incrementing across loads, so the same scene reloaded gives you different numbers. The constant at the top of an example, or the literal in its call, is there so the example has something to run against, and vrobots topic list is what tells you the truth.

Next: Hello control

See also: Truth, measured and believed, Pacing your loop, Timestamps and sequence numbers