Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pacing your loop

The two ways to decide how often your loop body runs, and how to pick between them.

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

Your clock or the data’s clock

Your program owns the loop, so something in the body has to decide when the next iteration starts. The SDK offers two answers and they are not interchangeable.

rate(hz) sleeps until your next tick, with drift compensation, and then returns. Your clock drives the loop. states() hands back whatever the latest snapshot is at that moment, which at a tick rate above the 25 Hz stream means the same sample twice and at a tick rate below it means samples you never look at.

wait_new_state(timeout) blocks until a snapshot newer than the one you have arrives. The data drives the loop, so the body runs once per published sample: no duplicates, no skips, and no need to guess a rate that divides 25 Hz.

flowchart TB
  subgraph A["rate(hz): your clock"]
    A1["read states()"] --> A2["compute and command"]
    A2 --> A3["rate(hz) sleeps to next tick"]
    A3 --> A1
    A4["may read the same sample twice"] -.-> A1
  end
  subgraph B["wait_new_state(t): the data's clock"]
    B1["wait_new_state(timeout)"] --> B2{"new sample?"}
    B2 -->|Ok| B3["read states(), compute"]
    B2 -->|Timeout| B4["note the stall, hold last state"]
    B3 --> B1
    B4 --> B1
  end

Both signatures come from the same handle:

crates/vrobots-sdk/src/robot.rs:

#![allow(unused)]
fn main() {
pub fn wait_new_state(&self, timeout: Duration) -> VrResult<()>

pub fn rate(&self, hz: f64)
}

Rust takes a Duration; C++ and Python take seconds as a plain double, and both default it to 0.2. Neither returns the sample in any of the three, so states() is still the read.

The call returns Ok(()) once a newer sample is available, at which point you still read it with states(). It does not hand you the sample, which keeps the read path identical in both styles.

A timeout is a status, not an error

VrError::Timeout from wait_new_state means “no new sample arrived in time”. The session is healthy, the subscriber is intact, and the next call may well succeed. A paused simulator, a stopped simulator and a very busy machine all announce themselves this way, and none of them is a reason to exit.

Propagating that error out of main with ? is the single most common way to turn a paused simulator into a crashed program. Match on it instead, and let every other variant be fatal.

examples/rust/src/bin/ex09_state_paced_loop.rs:

#![allow(unused)]
fn main() {
loop {
    match robot.wait_new_state(TIMEOUT) {
        Ok(()) => {
            // Exactly one new sample is waiting -- read it and do the work.
            let s = robot.states();
            let dt_ms = if last_t_ns == 0 {
                f64::NAN
            } else {
                (s.t_ns - last_t_ns) as f64 / 1e6
            };
            let skipped = s.seq.saturating_sub(last_seq + 1);
            last_seq = s.seq;
            last_t_ns = s.t_ns;

            let [x, y, z] = s.kin.lin_pos;
            println!(
                "seq={} dt={dt_ms:6.1} ms pos=({x:.3},{y:.2},{z:.2}){}",
                s.seq,
                if skipped > 0 {
                    format!("  <- {skipped} sample(s) skipped")
                } else {
                    String::new()
                }
            );
        }
        Err(VrError::Timeout(detail)) => {
            // Not a broken session: no sample arrived in time. The sim is
            // paused, stopped, or the machine is very busy. states() still
            // returns the last snapshot it had.
            let s = robot.states();
            println!(
                "no new state in {:?} ({detail}); still holding seq={} at t={:.3}",
                TIMEOUT, s.seq, s.elapsed
            );
        }
        Err(other) => return Err(other), // a real failure
    }
}
}

Rust’s match puts the two outcomes side by side; C++ inverts it, catching the timeout, rethrowing everything else, and falling through to the work. The continue in the timeout arm is what keeps the shape equivalent: a caught timeout must not run the read below it. The Python example has no timeout arm, so its loop is the success path alone.

Pausing the simulator while this runs switches the output from one line per sample to one line per timeout, and unpausing it switches back without a reconnect:

seq=310 dt=  40.0 ms pos=(0.031,0.85,-1.20)
seq=311 dt=  40.1 ms pos=(0.031,0.85,-1.20)
no new state in 200ms (no new state within 200ms (sys_id 1)); still holding seq=311 at t=12.440
no new state in 200ms (no new state within 200ms (sys_id 1)); still holding seq=311 at t=12.440
seq=312 dt=1240.3 ms pos=(0.031,0.85,-1.20)

The Python example prints no timeout lines, because the first timeout ends it. The traceback it prints instead ends with this line:

vrsdk.VrError: timeout: no new state for sys_id 1 within 200.000ms

The example sets TIMEOUT to 200 ms, five times the 25 Hz period. That is the useful shape for a timeout: long enough that ordinary jitter never trips it, short enough that a stall is noticed within a human’s patience.

Gotcha. wait_new_state guarantees that the sample is newer, not that it is the next one. The SDK stores the newest sample received, so if two arrive between wakeups you see the second and seq jumps by two. That is why the example computes skipped from seq rather than assuming one iteration equals one sample.

Choosing

Your loopUseBecause
a controller emitting a setpoint on a schedulerate(hz)the output must be periodic whatever the sensor did
a controller running faster than 25 Hzrate(hz)duplicate reads are free and the schedule is what matters
logging every sample oncewait_new_statea duplicated or missed row is a corrupt log
numerical differentiation or a filterwait_new_stateprocessing a sample twice gives a zero derivative and a wrong state
watching for the simulator to stallwait_new_stateit is the only call that reports the absence of data
a one-shot scriptneitherdo the request, print, exit

The two styles compose. A controller can run on rate(hz) and still call wait_new_state with a short timeout once a second to check that the stream is alive, which is the pattern Stream health builds on.

Note. rate(hz) is drift-compensated: its deadlines are absolute multiples of the period counted from the first call, not now + period, so the time your loop body spends working does not accumulate as a slow drift. An iteration that overruns its budget re-anchors to the present rather than firing a burst of zero-length sleeps to catch up. It is also deliberately infallible, so it needs no ? on the hot path: a non-finite or non-positive hz returns immediately with a warning instead of panicking.

Next: Stream health

See also: Timestamps and sequence numbers, The shape of a program, Measuring rates