The truck drivetrain

Steering limits, motor torque, brake torque and the pulse band, all clamped silently.

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

Truck only

srv/drive is the truck's own service. Ask any other robot for it and the query finds no responder: configure_drive returns VrError::NoResponder after service_timeout. That is a capability probe rather than a fault, and it is indistinguishable from a simulator that is not running, so confirm with vrobots topic list before concluding anything from it.

Every value is read live by the steering servo, the drive motor and the dynamics, so a change bites from the next physics step with no rebuild and no dropout.

DriveConfig

FieldTypeUnitsDefaultNotes
drive_modeOption<u32>None, untouched2 rear axle, 4 all wheels; anything else is ignored by the simulator, so the SDK refuses it first
max_steer_degOption<f64>degNone, untouchedwheel angle at full stick; hard-clamped to 0 to 60
steer_rate_dpsOption<f64>deg/sNone, untouchedservo sweep rate; 0 is an ideal, instantaneous servo
max_motor_torque_nmOption<f64>N·mNone, untouchedpeak torque per driven wheel
no_load_wheel_rpmOption<f64>rpmNone, untouchedwheel speed at full throttle with no load, so this is what sets top speed; <= 0 becomes 200
idle_brake_torque_nmOption<f64>N·mNone, untouchedper wheel, while the throttle sits in the deadband
max_brake_torque_nmOption<f64>N·mNone, untouchedper wheel, at a full brake command
pwm_bandOption<PwmBand>None, untouchedall four numbers move as one group

configure_drive refuses an empty config, a drive_mode that is not 2 or 4, and any non-finite value. Nothing else is checked, because nothing else can be: the clamps in the Notes column happen inside the simulator and never reach the ack.

PwmBand

FieldTypeUnitsFactory valueNotes
min_usu32µs1100full reverse, full left
neutral_usu32µs1500centre stick
max_usu32µs1900full forward, full right
deadband_usu32µshalf-width of the neutral deadband; inside it the throttle is idle and the idle brake torque holds the truck

If min_us < neutral_us < max_us does not hold, the simulator replaces the whole band with 1100 / 1500 / 1900. A partially sensible band is not something you can ask for.

Gotcha. The truck's factory band is 1100 / 1500 / 1900, while set_car validates against the wider 1100 to 2000 the actuators are specified on. So 1950 is accepted by the SDK and is past full throttle for this truck. There is no way to move the neutral point alone.

Measuring instead of reading back

There is nothing to read back, so ex26 drives the same full-left circle four times and compares the steady turn radius, speed / yaw_rate. A steering limit that halves must roughly double the radius, or the request did not land.

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

#![allow(unused)]
fn main() {
    // ===== run 2: half the steering =====
    robot.configure_drive(&DriveConfig::default().with_max_steer_deg(15.0))?;
    let narrow = circle(&robot, "max_steer_deg = 15")?;

    // ===== run 3: more than the simulator allows =====
    robot.configure_drive(&DriveConfig::default().with_max_steer_deg(90.0))?;
    let clamped = circle(&robot, "max_steer_deg = 90 -> clamped to 60")?;
}
The same in C++ (examples/cpp/ex26_drive_config.cpp)
// ===== run 2: half the steering =====
{
    auto config = vrsdk::drive_config();
    config.has_max_steer_deg = true;
    config.max_steer_deg = 15.0;
    robot.configure_drive(config);
}
const Circle narrow = circle(robot, "max_steer_deg = 15");

// ===== run 3: more than the simulator allows =====
{
    auto config = vrsdk::drive_config();
    config.has_max_steer_deg = true;
    config.max_steer_deg = 90.0;
    robot.configure_drive(config);
}
const Circle clamped = circle(robot, "max_steer_deg = 90 -> clamped to 60");
The same in Python (examples/python/ex26_drive_config.py)
# ===== run 2: half the steering =====
robot.configure_drive(max_steer_deg=15.0)
narrow = circle(robot, "max_steer_deg = 15")

# ===== run 3: more than the simulator allows =====
robot.configure_drive(max_steer_deg=90.0)
clamped = circle(robot, "max_steer_deg = 90 -> clamped to 60")

One field costs one call in Rust and Python and three lines in C++, because the C++ surface is a plain struct: take an empty request from vrsdk::drive_config(), then set the value and its has_* flag together. The flag is the load-bearing half. A value written without it goes out as if the field had never been touched, and the ack looks the same either way.

Run 2 widens the circle. Run 3 asks for 90 degrees, is acked ok, and drives the circle of a truck limited to 60:

steady turn radius (speed / yaw rate), same command every time:
  as spawned                             r=<m>   speed=<m/s>  yaw=<rad/s>  servo=<value>
  max_steer_deg = 15                     r=<m>   speed=<m/s>  yaw=<rad/s>  servo=<value>
  max_steer_deg = 90 -> clamped to 60    r=<m>   speed=<m/s>  yaw=<rad/s>  servo=<value>
  drive_mode = 2, 40 N.m, 30 deg         r=<m>   speed=<m/s>  yaw=<rad/s>  servo=<value>

The 90 that came back as 60 is indistinguishable from a 60 that was asked for. The circle is the only witness.

Setting the whole drivetrain at once

Run 4 fills in every field, including the factory band restated explicitly.

#![allow(unused)]
fn main() {
    robot.configure_drive(
        &DriveConfig::default()
            .with_drive_mode(2) // rear axle only (4 = all wheels)
            .with_max_steer_deg(30.0)
            .with_steer_rate_dps(120.0) // 0 would be an ideal, instant servo
            .with_max_motor_torque_nm(40.0)
            .with_no_load_wheel_rpm(200.0)
            .with_idle_brake_torque_nm(5.0)
            .with_max_brake_torque_nm(150.0)
            // All four numbers move together, and this IS the factory band.
            .with_pwm_band(PwmBand::new(1100, 1500, 1900, 30)),
    )?;
}
The same in C++ (examples/cpp/ex26_drive_config.cpp)
// ===== run 4: rear-wheel drive, softer motor, factory band restated ====
{
    auto config = vrsdk::drive_config();
    config.has_drive_mode = true;
    config.drive_mode = 2;  // rear axle only (4 = all wheels)
    config.has_max_steer_deg = true;
    config.max_steer_deg = 30.0;
    config.has_steer_rate_dps = true;
    config.steer_rate_dps = 120.0;  // 0 would be an ideal, instant servo
    config.has_max_motor_torque_nm = true;
    config.max_motor_torque_nm = 40.0;
    config.has_no_load_wheel_rpm = true;
    config.no_load_wheel_rpm = 200.0;
    config.has_idle_brake_torque_nm = true;
    config.idle_brake_torque_nm = 5.0;
    config.has_max_brake_torque_nm = true;
    config.max_brake_torque_nm = 150.0;
    // All four numbers move together, and this IS the factory band.
    config.has_pwm_band = true;
    config.pwm_band = vrsdk_pwm_band_t{1100, 1500, 1900, 30};
    robot.configure_drive(config);
}
The same in Python (examples/python/ex26_drive_config.py)
# ===== run 4: rear-wheel drive, softer motor, factory band restated =====
robot.configure_drive(
    drive_mode=2,  # rear axle only (4 = all wheels)
    max_steer_deg=30.0,
    steer_rate_dps=120.0,  # 0 would be an ideal, instant servo
    max_motor_torque_nm=40.0,
    no_load_wheel_rpm=200.0,
    idle_brake_torque_nm=5.0,
    max_brake_torque_nm=150.0,
    # All four numbers move together, and this IS the factory band.
    pwm_band=PwmBand(1100, 1500, 1900, 30),
)

The pulse-width band is the one member that is written whole on every surface, because all four numbers travel as a group: PwmBand::new(...) in Rust, PwmBand(...) in Python, and a braced vrsdk_pwm_band_t in C++. There is no keep-current for one number inside it, so the C++ flag has_pwm_band decides only whether the whole block is sent.

The truck keeps driving through the change: no dropout, no re-spawn, and one line of echo at the start of the next circle.

Reading the drivetrain in the state stream

ChannelMeaning
actuator.pwmthe three channels you sent: steer, throttle, brake
actuator.measured[0..3]the four wheel speeds in rad/s, FL, FR, RL, RR
actuator.measured[4]the steering servo, which is the channel that answers max_steer_deg

An undriven wheel still reports, because the road turns it. So drive_mode 2 versus 4 shows up as which wheels lead under power, not as two silent channels.

Note. There is also a selector form of this service (?mode=2|4) for clients that cannot attach a payload. The SDK can, so it does not use it.

What the SDK refuses

-- refused before anything reaches the wire --
  drive_mode = 3   [<code>] <message>
  nothing set      [<code>] <message>

Next: Rotors and thrust curves

See also: Driving the truck, Truck, Actuators