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
| Verb | How you issue it | Confirmed by |
|---|---|---|
| create | connect(type, None) | the new robot’s state topic starting to publish |
| attach | connect(type, Some(id)) | the first state snapshot, which connect blocks for |
| activate | ConnectOptions::activate_after_create, inside connect | the ack, and then the state topic |
| reset | reset() | the position in the state stream, one step later |
| delete | delete() | 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 asok = falsefor an unknownsys_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/resetis 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 by | Survives a reset? |
|---|---|
set_physical_params (mass, inertia) | yes |
configure_sensors (noise models) | yes |
configure_rotors, configure_drive, configure_msd, configure_cartpole | yes |
set_frames, set_skin | yes |
| position, orientation, velocity | no, teleported home |
| the latched command and the actuator echo | no, re-latched to the initial command |
| fixed-wing control mode and estimate source | no, reverted to onboard and truth |
seq and elapsed | untouched, 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