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.
| Robot | Keys |
|---|---|
| Multirotor | blue, desert, gold, green, mono, pink, snow, white |
| Truck | black, blue, camouflage, gray, red |
| everything else | none, 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.
| Request | Reply | What actually happened |
|---|---|---|
| a key your tier allows | ok | the skin changed |
| any key, tier too low | ok = false plus a reason | VrError::Service; do not retry |
gold on a truck (a multirotor key) | ok | nothing, logged inside the simulator |
chartreuse (in no catalog) | ok | nothing, 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. The Rust and C++ versions of ex23 treat it as final
and stop walking the list rather than asking four more times.
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),
}
}
Rust matches the VrError::Service variant and hands every other variant back to the caller. C++
has one error type, so it catches it, compares the code against VRSDK_ERR_SERVICE, and rethrows
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.
The Python script prints set_skin('black') with no verdict, followed by two samples of the
hold. A refusal appears as the traceback of the uncaught vrsdk.VrError instead of a line.
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. The Rust and C++ versions of ex23
demonstrate 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
}
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++ helper returns a plain bool and lets 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.
That is the Rust and C++ output. The Python script prints nothing for its gold request.
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.
| Channel | Meaning |
|---|---|
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 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>)
The Python script prints set_skin('camouflage') and then samples without the servo:
t=<seconds>s speed=<m/s> wheels=[<w0>, <w1>, <w2>, <w3>].
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 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