Skins

The only service that ever tells you no, and the one where a wrong name looks like success.

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

The catalogs

set_skin(&str) takes a catalog key and dresses the robot in it. The catalogs belong to the robot type and are matched case-insensitively.

RobotKeys
Multirotorblue, desert, gold, green, mono, pink, snow, white
Truckblack, blue, camouflage, gray, red
everything elsenone, so every request is a no-op

An empty or whitespace-only name is refused client-side with VrError::InvalidArgument, because on the wire an empty payload is a read-back probe rather than a skin.

The one refusal in the whole API

Skins are tier-gated inside the simulator. A tier refusal comes back as an honest ok = false with a reason, which the SDK surfaces as VrError::Service carrying the simulator's own message. That is the single place in this API surface where a service says no.

RequestReplyWhat actually happened
a key your tier allowsokthe skin changed
any key, tier too lowok = false plus a reasonVrError::Service; do not retry
gold on a truck (a multirotor key)oknothing, logged inside the simulator
chartreuse (in no catalog)oknothing, logged inside the simulator

Do not retry a VrError::Service from this service. It is tier-gated rather than transient, so the answer will not change. ex23 treats it as final and stops walking the list rather than asking four more times.

From examples/rust/src/bin/ex23_skins.rs:

#![allow(unused)]
fn main() {
    match robot.set_skin(skin) {
        Ok(()) => println!("set_skin({skin:?}) -> ok"),
        Err(VrError::Service(reason)) => {
            // The sim's own words. This is the ONLY service that ever gets here.
            println!("set_skin({skin:?}) -> REFUSED by the sim: {reason}");
            return Ok(false);
        }
        Err(other) => return Err(other),
    }
}
The same in C++ (examples/cpp/ex23_skins.cpp)
try {
    robot.set_skin(skin);
    std::printf("set_skin(\"%s\") -> ok\n", skin.c_str());
} catch (const vrsdk::Error& e) {
    // The sim's own words. This is the ONLY service that ever gets here, and
    // only for a tier refusal -- anything else is a real failure.
    if (e.code() != VRSDK_ERR_SERVICE) {
        throw;
    }
    std::printf("set_skin(\"%s\") -> REFUSED by the sim: %s\n", skin.c_str(), e.what());
    return false;
}
The same in Python (examples/python/ex23_skins.py)
try:
    robot.set_skin(skin)
    print(f"set_skin({skin!r}) -> ok")
except vrsdk.VrError as e:
    # The sim's own words. This is the ONLY service that ever gets here, and
    # only for a tier refusal -- anything else is a real failure.
    if e.code != vrsdk.err.SERVICE:
        raise
    print(f"set_skin({skin!r}) -> REFUSED by the sim: {e.detail}")
    return False

Rust matches the VrError::Service variant and hands every other variant back to the caller. C++ and Python have one error type each, so they catch it, compare the code against VRSDK_ERR_SERVICE or vrsdk.err.SERVICE, and rethrow anything that is not a tier refusal.

A permitted key prints one line and the truck changes colour. A refused one prints the simulator's own explanation and the run stops:

set_skin("black") -> ok
set_skin("blue") -> REFUSED by the sim: <the simulator's message>

Stopping: the tier gate does not open on a retry.

Note. Every other refusal in this chapter is silent. This one is the exception, and it is worth knowing precisely because of what the rest do instead.

A typo looks like success

An unknown key on a robot that has a catalog is acked ok and dropped with a log line no client can see. So is a key from another robot's catalog. ex23 demonstrates both, after walking the five real truck keys:

#![allow(unused)]
fn main() {
    println!("\n-- keys that are acked `ok` and dropped inside the simulator --");
    wear(&robot, WRONG_TYPE_SKIN)?; // a multirotor key, on a truck
    wear(&robot, UNKNOWN_SKIN)?; // no catalog has it
}
The same in C++ (examples/cpp/ex23_skins.cpp)
std::printf("\n-- keys that are acked `ok` and dropped inside the simulator --\n");
wear(robot, WRONG_TYPE_SKIN);  // a multirotor key, on a truck
wear(robot, UNKNOWN_SKIN);     // no catalog has it
The same in Python (examples/python/ex23_skins.py)
print("\n-- keys that are acked `ok` and dropped inside the simulator --")
wear(robot, WRONG_TYPE_SKIN)  # a multirotor key, on a truck
wear(robot, UNKNOWN_SKIN)  # no catalog has it

Rust's wear returns a Result, so these two calls still carry ? to propagate a genuine error even though the bool is dropped. The C++ and Python helpers return a plain bool and let an unexpected error unwind on its own. Neither key raises anything here: both come back ok.

Both return Ok(()), and the truck is still wearing the last key that worked:

-- keys that are acked `ok` and dropped inside the simulator --
set_skin("gold") -> ok
set_skin("chartreuse") -> ok
Both returned Ok. The truck is still wearing "red" -- the ack was a receipt for a request the robot then refused with a log line no client can see.

The confirmation is the robot in front of you. There is no read-back and no state field carrying the current skin.

On a truck a skin is not only cosmetic

The wheel colliders travel with the skin prefab, so a swap rebinds the physics wheels. ex23 keeps the truck rolling across every change so that shows up on the wire.

ChannelMeaning
actuator.measured[0..3]the four wheel speeds, FL, FR, RL, RR, in rad/s
actuator.measured[4]the steering servo
#![allow(unused)]
fn main() {
    for i in 0..HOLD_SAMPLES {
        robot.set_car(STEER_US, THROTTLE_US, Some(BRAKE_US))?;
        if i % 15 == 0 {
            let s = robot.states();
            let [vx, vy, vz] = s.kin.lin_vel;
            println!(
                "    t={:6.2}s speed={:5.2} m/s wheels={:?} steer_servo={:?}",
                s.elapsed,
                (vx * vx + vy * vy + vz * vz).sqrt(),
                // 0..3 are FL, FR, RL, RR in rad/s; they must keep turning
                // across the swap, because the colliders were just rebound.
                &s.actuator.measured[..s.actuator.measured.len().min(4)],
                s.actuator.measured.get(4)
            );
        }
        robot.rate(HZ);
    }
}
The same in C++ (examples/cpp/ex23_skins.cpp)
for (int i = 0; i < HOLD_SAMPLES; ++i) {
    robot.set_car(STEER_US, THROTTLE_US, BRAKE_US);
    if (i % 15 == 0) {
        const vrsdk::State s = robot.states();
        const double* v = s.kin().lin_vel;
        const double speed = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
        // 0..3 are FL, FR, RL, RR in rad/s; they must keep turning across
        // the swap, because the colliders were just rebound.
        std::printf("    t=%6.2fs speed=%5.2f m/s wheels=%s steer_servo=%s\n", s.elapsed,
                    speed, channels(s.actuator(), 0, 4).c_str(),
                    channels(s.actuator(), 4, 5).c_str());
    }
    robot.rate(HZ);
}
The same in Python (examples/python/ex23_skins.py)
for i in range(HOLD_SAMPLES):
    robot.set_car(STEER_US, THROTTLE_US, BRAKE_US)
    if i % 15 == 0:
        s = robot.states
        speed = math.dist(s.kin.lin_vel, (0.0, 0.0, 0.0))
        m = s.actuator.measured
        # 0..3 are FL, FR, RL, RR in rad/s; they must keep turning across
        # the swap, because the colliders were just rebound.
        wheels = [round(v, 3) for v in m[:4]]
        servo = [round(v, 3) for v in m[4:5]]
        print(
            f"    t={s.elapsed:6.2f}s speed={speed:5.2f} m/s "
            f"wheels={wheels} steer_servo={servo}"
        )
    robot.rate(HZ)

The brake is an Option in Rust and a plain number in the other two. Reading the wheel channels also differs: Rust and Python slice a growable list, while C++ has a fixed array with a separate measured_count, which is why ex23 gives its channels helper a first and last index to stay inside it.

The four wheel channels must keep turning across each swap. A wheel that flatlines is a rebind that did not take:

set_skin("camouflage") -> ok
    t=<seconds>s speed=<m/s> wheels=[<w0>, <w1>, <w2>, <w3>] steer_servo=Some(<value>)

That is also why a skin swap is worth doing while the truck is stationary if you care about repeatability: it is a change to the physics rig, not a texture swap.

The empty key

The name is trimmed before it is checked, so whitespace does not sneak past.

#![allow(unused)]
fn main() {
    match robot.set_skin("   ") {
        Ok(()) => println!("\nUNEXPECTED: an empty key was accepted"),
        Err(e) => println!("\nempty key -> [{}] {}", e.code(), e.detail()),
    }
}
The same in C++ (examples/cpp/ex23_skins.cpp)
try {
    robot.set_skin("   ");
    std::printf("\nUNEXPECTED: an empty key was accepted\n");
} catch (const vrsdk::Error& e) {
    std::printf("\nempty key -> [%d] %s\n", e.code(), e.what());
}
The same in Python (examples/python/ex23_skins.py)
try:
    robot.set_skin("   ")
    print("\nUNEXPECTED: an empty key was accepted")
except vrsdk.VrError as e:
    print(f"\nempty key -> [{e.code} {e.kind}] {e.detail}")

The error carries the same code and message everywhere, but you read it differently: e.code() with e.detail() in Rust, e.code() with e.what() in C++, and the attributes e.code, e.kind and e.detail in Python.

empty key -> [<code>] <message>

Next: Supported virtual robots

See also: Truck, Actuators, Appendix C: Error reference