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.

From 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(())
}
The same in C++ (examples/cpp/ex04_hello_service.cpp)
int main() {
    try {
        vrsdk::check_version();

        // Create a NEW robot in the sim: `create` means "no sys_id", so the
        // manager assigns one and the reply carries it. (A constructor would be
        // ambiguous with the attach form -- see the header.)
        vrsdk::VirtualRobot robot = vrsdk::VirtualRobot::create(vrsdk::RobotType::Multirotor);
        robot.connect();
        const std::uint32_t sys_id = robot.sys_id();
        std::printf("created sys_id = %u\n", 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.
        const vrsdk::State s = robot.states();
        std::printf("first state: t=%.3f seq=%llu name=\"%s\"\n", s.elapsed,
                    static_cast<unsigned long long>(s.seq), s.name.c_str());
        // The C++ surface has no topic-name builder (Rust has
        // `vrobots_sdk::topics`, Python has `vrsdk.topics`), and the shape is
        // fixed by the wire, so compose it here.
        std::printf("its state topic: vrobots/%u/z/state\n", sys_id);

        // Deletion is explicit and never implicit. The manager's ack is only a
        // receipt, so remove() also waits for the robot's state topic to fall
        // silent -- that is the real confirmation.
        robot.remove();
        std::printf("deleted sys_id = %u (removed=%s)\n", sys_id,
                    robot.removed() ? "true" : "false");

        // The handle is spent. Commands do not silently do nothing -- they say
        // why.
        try {
            robot.set_mr_pwm({1500.0, 1500.0, 1500.0, 1500.0});
            std::printf("unexpected: a deleted robot accepted a command\n");
        } catch (const vrsdk::Error& e) {
            std::printf("the handle is spent, as expected: [%d] %s\n", e.code(), e.what());
        }
        return 0;
    } catch (const vrsdk::Error& e) {
        std::fprintf(stderr, "error [%d] %s\n", e.code(), e.what());
        return 1;
    }
}
The same in Python (examples/python/ex04_hello_service.py)
ROBOT_TYPE = RobotType.MULTIROTOR  # or RobotType.TRUCK / RobotType.from_key("truck")


def main() -> None:
    vrsdk.init_logging("info")

    # Create a NEW robot in the sim (no sys_id -> manager create; the reply
    # carries the assigned id).
    robot = VirtualRobot(ROBOT_TYPE)
    robot.connect()
    sys_id = robot.sys_id
    print(f"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.
    s = robot.states
    print(f"first state: t={s.elapsed:.3f} seq={s.seq} name={s.name!r}")
    print(f"its state topic: {vrsdk.topics(sys_id)['state']}")

    # 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()
    print(f"deleted sys_id = {sys_id} (is_deleted={robot.is_deleted})")

    # The handle is spent. Commands do not silently do nothing -- they say why.
    try:
        robot.set_mr_pwm(1500, 1500, 1500, 1500)
        print("unexpected: a deleted robot accepted a command")
    except vrsdk.VrError as e:
        print(f"the handle is spent, as expected: [{e.code} {e.kind}] {e.detail}")

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 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

Create versus attach

The second argument to connect decides which of two quite different things happens.

ArgumentWhat it doesTouches srv/create
Some(id)attaches to a robot the scene already containsno
Noneasks the manager to spawn a new one; the reply carries the idyes

Every example so far passed Some(SYS_ID). This one passes None, 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.

Next: When nothing happens

See also: Robot lifecycle, System ids, and the two kinds of robot, Appendix C: Error reference