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

Robot lifecycle

Create, attach, activate, reset and delete, and what each is confirmed by.

cargo run -p vrobots-examples --bin ex04_hello_service
./target/cpp-build/ex04_hello_service
python examples/python/ex04_hello_service.py
cargo run -p vrobots-examples --bin ex21_reset -- 1
./target/cpp-build/ex21_reset 1
python examples/python/ex21_reset.py 1
cargo run -p vrobots-examples --bin ex21_reset
./target/cpp-build/ex21_reset
python examples/python/ex21_reset.py

ex21_reset takes an optional sys_id in all three languages. With it, the example attaches to the scene’s own multirotor and leaves it running; without it, the example creates one and deletes it at the end. Prefer the argument: a client-created multirotor does not integrate physics in simulator v3.0.0, which is a known issue.

Five verbs, five different confirmations

VerbHow you issue itConfirmed by
createconnect(type, None)the new robot’s state topic starting to publish
attachconnect(type, Some(id))the first state snapshot, which connect blocks for
activateConnectOptions::activate_after_create, inside connectthe ack, and then the state topic
resetreset()the position in the state stream, one step later
deletedelete()the state topic going silent for one second

There is no public activate() method. Activation happens as step 3 of the create sequence when activate_after_create is true, which it is by default. Attaching never activates, because the scene already did.

Create and delete

connect(type, None) asks the manager to spawn a robot and the reply carries its sys_id. Create is the only non-idempotent service in the system, so the SDK sends it exactly once and never retries: a retry that lands spawns a second robot.

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

#![allow(unused)]
fn main() {
    // Create a NEW robot in the sim (no sys_id -> manager create; reply carries the id).
    let robot = VirtualRobot::connect(ROBOT_TYPE, None)?;
    let sys_id = robot.sys_id();
    println!("created sys_id = {sys_id}");
}

C++ and Python build the handle first and call connect() on it, where Rust’s VirtualRobot::connect does both in one call. C++ spells the create form VirtualRobot::create(type) because a one-argument constructor would be ambiguous with the attach form, and Python reads sys_id as a property rather than a method.

By the time connect returns, the robot’s state topic has published at least once, so the snapshot the next lines read is real data rather than a placeholder:

created sys_id = <id>
first state: t=<seconds> seq=<n> name=<robot name>
its state topic: vrobots/<id>/z/state

Deletion is explicit and never implicit. Dropping a VirtualRobot closes the session and leaves the robot running, which is the point of the fourth rule: robots outlive the process.

#![allow(unused)]
fn main() {
    // Deletion is explicit and never implicit. delete() waits for the state topic
    // to fall silent: the manager's ack is only a receipt, absence is the proof.
    robot.delete()?;
    println!(
        "deleted sys_id = {sys_id} (is_deleted={})",
        robot.is_deleted()
    );
}

Only the names differ. C++ spells the pair remove() and removed() because delete is a keyword, and Python’s is_deleted is a property where Rust’s is a method.

The call returns once the state topic has been quiet for one second, which at 25 Hz is 25 missing samples:

deleted sys_id = <id> (is_deleted=true)

C++ prints (removed=true) and Python (is_deleted=True). The Python script ends here.

Note. delete() is the one service the SDK deliberately does not retry. A re-send after a delete the manager already applied comes back as ok = false for an unknown sys_id, which would turn a successful delete into a reported failure.

After that the handle is spent. Every command and every service on it fails with VrError::Deleted rather than doing nothing quietly.

#![allow(unused)]
fn main() {
    match robot.set_mr_pwm([1500.0; 4]) {
        Ok(()) => println!("unexpected: a deleted robot accepted a command"),
        Err(e) => println!("the handle is spent, as expected: [{}] {e}", e.code()),
    }
}

Rust returns the refusal as a Result you match on, while C++ throws vrsdk::Error, so the C++ call sits inside a try.

the handle is spent, as expected: [<code>] <message>

Reset

reset() teleports the robot to the pose captured at its first physics step, zeroes linear and angular velocity, rests the actuators and re-latches the robot’s initial command. It is what the simulator’s own Reset button does.

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

#![allow(unused)]
fn main() {
    let before = robot.states();
    println!("\n-- reset() (a bare GET) --");
    robot.reset()?;
    println!(
        "acked. That is a RECEIPT: the teleport lands in phase 0 of the next \
         physics step, and the state stream is the proof."
    );
}

reset() takes no arguments and returns nothing in any of the three, so the only difference is the snapshot beside it: robot.states is a property in Python where Rust and C++ call states(). Rust and C++ print the receipt as a line of output; the Python script keeps it as the comment on the call and goes straight to the 50 settling samples.

The example stops commanding across the reset, so the effect is visible: with nothing sent, the actuator echo falls from the climb pulse back to the robot’s initial 1100 us idle, and the distance from home collapses to roughly zero.

-- reset() (a bare GET) --
acked. That is a RECEIPT: the teleport lands in phase 0 of the next physics step, and the state stream is the proof.
home? seq=<n> t=<seconds>s pos=(...) [frd] alt=<metres> |v|=<speed> echo=[...]  d(home)=<metres> m

The Python script prints no per-sample lines. It ends with the same three before-and-after comparisons the Rust and C++ runs end with, in a shorter form, followed by deleted sys_id=<id> when it created the robot:

position:  <metres> m from home before, <metres> m after
actuators: echo [...] -> [...]
time:      seq <n> -> <n>, elapsed <seconds>s -> <seconds>s

A live publisher wins one step later. A control loop that keeps sending 1700 us climbs straight back out of the reset and barely registers that it happened, which is exactly what you want when the loop under test is the thing you are resetting around.

Gotcha. srv/reset is the one service key where a payload-less GET performs the action instead of probing it. The simulator’s vendored C# zenoh client cannot attach a payload, so an empty query had to mean something. Probe every other key freely; never probe this one. Because twice home is still home, reset() is idempotent and its discovery-retry loop is safe.

Home is not where you found it

Home is the pose at the robot’s first physics step, not the pose you attached at. A scene robot that has been flying since the scene loaded can be a long way from it: measured live, one attach found a multirotor 27 m from its home, and a program that treated the attach position as home reported the reset as having moved the robot away from where it belonged.

Nothing reads the home pose out, so the only honest way to learn it is to go there.

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

#![allow(unused)]
fn main() {
fn learn_home(robot: &VirtualRobot, created: bool) -> Result<State, VrError> {
    if created {
        return Ok(robot.states());
    }

    println!("attached: resetting once to find out where home actually is");
    robot.reset()?;
    for _ in 0..SETTLE_SAMPLES {
        robot.rate(HZ);
    }
    Ok(robot.states())
}
}

Rust and C++ wrap the step in a learn_home function that skips it on the create path, because nothing has happened to a new robot yet and its first sample already is home. Both hand the snapshot back by value, an owned State in Rust and a vrsdk::State in C++. The Python script runs the same three steps inline on both paths, which costs nothing on a created robot, since twice home is still home. The same trick finds a cart pole’s rail centre, which is the one number that plant needs and does not publish.

What survives a reset

Set bySurvives a reset?
set_physical_params (mass, inertia)yes
configure_sensors (noise models)yes
configure_rotors, configure_drive, configure_msd, configure_cartpoleyes
set_frames, set_skinyes
position, orientation, velocityno, teleported home
the latched command and the actuator echono, re-latched to the initial command
fixed-wing control mode and estimate sourceno, reverted to onboard and truth
seq and elapseduntouched, the clock never restarts

A frozen elapsed means the simulator stopped, never that something reset.

Configuration surviving is what makes attaching to a scene robot a one-way door. There is no getter for mass, inertia or rotor geometry, so the SDK cannot read the old value and put it back, and reset() will not do it for you. Whatever you configure on a shared robot stays configured for every other client until the scene is reloaded.

Next: Mass and inertia

See also: What connect actually does, System ids, and the two kinds of robot, Known simulator issues