Hello service
Create a robot from code and delete it again, which is what explicit lifecycle means.
cargo run -p vrobots-examples --bin ex04_hello_service
./target/cpp-build/ex04_hello_service
python examples/python/ex04_hello_service.py
The whole program
Lifecycle and configuration are one-shot request and response, so this is the first example with no loop in it.
examples/rust/src/bin/ex04_hello_service.rs:
use vrobots_sdk::{RobotType, VirtualRobot, VrError};
const ROBOT_TYPE: RobotType = RobotType::Multirotor;
fn main() -> Result<(), VrError> {
vrobots_sdk::init_logging("info");
// 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}");
// The create reply is a receipt; the robot *exists* once its state topic
// publishes, which connect() already waited for -- so this is real data.
let s = robot.states();
println!(
"first state: t={:.3} seq={} name={:?}",
s.elapsed, s.seq, s.name
);
println!("its state topic: {}", vrobots_sdk::topics::state(sys_id));
// 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()
);
// The handle is spent. Commands do not silently do nothing -- they say why.
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()),
}
Ok(())
}
Three differences worth naming. C++ spells creation VirtualRobot::create rather than a
one-argument constructor, because a literal 0 would be ambiguous between “attach to sys_id
0” and a null options pointer. C++ spells deletion remove(), because delete is a keyword.
And C++ has no topic-name builder, so it composes vrobots/<id>/z/state inline where Rust
calls topics::state and Python calls vrsdk.topics.
The Python script is shorter in one respect: it stops after the delete, where Rust and C++ go on to send one command on the spent handle.
The run takes a second or two, most of it spent waiting for the new robot’s state topic to start and then to stop:
created sys_id = 7
first state: t=0.000 seq=0 name="multirotor"
its state topic: vrobots/7/z/state
deleted sys_id = 7 (is_deleted=true)
the handle is spent, as expected: [8] deleted: sys_id 7 was deleted from the sim; this handle is spent
That is the Rust run. C++ prints removed=true where Rust prints is_deleted=true, and the
Python script prints the first four lines in Python’s own spelling, name='multirotor' and
is_deleted=True.
Create versus attach
The second argument to connect decides which of two quite different things happens.
| Argument | What it does | Touches srv/create |
|---|---|---|
Some(id) | attaches to a robot the scene already contains | no |
None | asks the manager to spawn a new one; the reply carries the id | yes |
Every example so far attached by id, which is Some(SYS_ID) in Rust. This one passes
None, which in Python means leaving sys_id out, so the manager allocates the id and
robot.sys_id() is the only way to learn it.
Creating is limited by the scene’s catalog, not the SDK’s. The sandbox scene registers
multirotor, truck and msd; any other key is refused with a message naming the ones it
does know, which is also the only live way to enumerate the catalog. Robot types that are
scene-authored only, such as the cart pole and the Global Hawk, can be attached to but
never created.
Note. Create is the one non-idempotent service in the system. The SDK sends it exactly once and never retries, because every retry that reaches the manager reserves another id and spawns another robot. What connect actually does walks the four steps.
Robots outlive the process
Dropping a VirtualRobot closes its zenoh session. It does not delete the robot. The
robot keeps flying its last latched command, keeps publishing state, and is still there
after your program exits, after you rebuild, and after you run something else.
This is why the example calls delete(): without it, every run would leave another
multirotor in the scene. Comment the delete() line out and run it twice to see exactly
that, then attach ex01_hello_states to one of the ids it printed. Create in one process,
attach from another, is the normal shape of a multi-program session.
The way back from a littered scene is to reload it. Ids are allocated at scene load and keep incrementing, so the reloaded scene numbers its robots differently.
Absence is the proof, both ways
connect(type, None) returns only once the new robot’s state topic has published, and
delete() returns only once that topic has fallen silent. Neither waits on the service
acknowledgement, because an acknowledgement is packed the instant the request arrives and
says nothing about whether the work happened.
That principle runs through the whole services chapter: an ack is a receipt, not a result, and the state stream is the confirmation. It is rule two of Five rules that explain everything.
After delete() the handle is spent, and it says so rather than failing quietly. Every
command on it returns VrError::Deleted, error code 8, with a message naming the id; the
last line of the Rust and C++ runs shows it.
Next: When nothing happens
See also: Robot lifecycle, System ids, and the two kinds of robot, Appendix C: Error reference