Chassis — the frozen facade¶
The public surface every autonomous routine is written against. Frozen as register row F6 on 2026-08-12: every member below changes only with a major API-version bump plus a migration note.
Extracted from include/shulib/chassis/chassis.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.
Contents¶
struct ChassisConfigmotionschedulerstruct MotionOptionstimeoutmaxLinearSpeedmaxAngularSpeedvalidatestruct TrajectoryResultexitcompletedLegstotalLegssucceededclass ChassisChassisChassis (overload 2)Chassis (overload 3)operator=operator= (overload 2)~ChassismoveTostrafeToturnTofollowTrajectoryfollowTrajectory (overload 2)brakeholdwaitdrivecancelwaitUntilposesetPosestrafeAuthoritylastExitReasonlastCompletedmotionConfigdepsschedulerscheduler (overload 2)
struct ChassisConfig¶
Everything configurable about a Chassis, in one place. Both members are the lower layers' own config types passed through WHOLE — so an additive field there (e.g. a future per-wheel speed budget in MotionConfig, the C3 §11 flag) flows through this surface with no reshape.
Declared at include/shulib/chassis/chassis.hpp:173.
ChassisConfig::motion¶
gains/budgets/tolerances (HA-50/51/52)
field, declared at include/shulib/chassis/chassis.hpp:174.
ChassisConfig::scheduler¶
fault policy mask + loop monitor
field, declared at include/shulib/chassis/chassis.hpp:175.
struct MotionOptions¶
Per-call knobs for the blocking verbs. 0 (the default) = "use the ChassisConfig value". Validated finite and >= 0 at each call. FROZEN F6 NOTE (D2): the fields BELOW are frozen (name/type/meaning); the field SET is deliberately additive-open — a future knob is a new field with a 0/"config default" meaning, never a reshape of these.
Declared at include/shulib/chassis/chassis.hpp:184.
MotionOptions::timeout¶
Watchdog bound for this motion, INCLUDING any boot wait. Typed time (D2): {.timeout = 5_s} / {.timeout = 500_ms} — a bare double does not compile, so "500 meaning milliseconds" cannot silently become 500 seconds of match time.
field, declared at include/shulib/chassis/chassis.hpp:189.
MotionOptions::maxLinearSpeed¶
Field-frame linear speed budget for this motion (in/s) — the norm cap AND the base of the strafe-authority clamp, exactly as in MotionConfig. The per-wheel budget (maxWheelSpeed) is deliberately NOT scaled with it: that is a hardware envelope, not a per-leg intent.
field, declared at include/shulib/chassis/chassis.hpp:194.
MotionOptions::maxAngularSpeed¶
Yaw-rate budget for this motion (rad/s).
field, declared at include/shulib/chassis/chassis.hpp:196.
MotionOptions::validate¶
Reject nonsense before anything moves: every field must be finite and >= 0. Called by each verb at the door, so a bad option value is a loud error at the call site rather than a mystery mid-motion.
function, declared at include/shulib/chassis/chassis.hpp:201.
struct TrajectoryResult¶
What followTrajectory did — which leg count it completed and how the last attempted leg exited. (ExitReason alone would lose WHERE the chain broke; the next thing a routine does after a failed trajectory legitimately depends on how far it got.)
Declared at include/shulib/chassis/chassis.hpp:217.
TrajectoryResult::exit¶
last attempted leg's verdict
field, declared at include/shulib/chassis/chassis.hpp:218.
TrajectoryResult::completedLegs¶
legs that SETTLED (== totalLegs on success)
field, declared at include/shulib/chassis/chassis.hpp:219.
TrajectoryResult::totalLegs¶
waypoints given
field, declared at include/shulib/chassis/chassis.hpp:220.
TrajectoryResult::succeeded¶
True only if the last attempted leg SETTLED and every leg was completed. Note what this means for a value-initialized TrajectoryResult (0 of 0 legs, exit Settled): it reads as success. That is correct here — this verb requires at least one waypoint, so a result it produces always has legs — but any code that holds a TrajectoryResult BEFORE running one must initialize exit to Running instead (Routine::lastTrajectory does).
function, declared at include/shulib/chassis/chassis.hpp:227.
class Chassis¶
The public facade every autonomous routine is written against: the blocking motion verbs, the frame-explicit manual verb, control, state, and the Tier-3 seam — over one owned MotionScheduler. FROZEN (register row F6, locked 2026-08-12); the file banner above carries the design reasoning behind every shape here, and is meant to be read before changing anything.
Declared at include/shulib/chassis/chassis.hpp:237.
Chassis::Chassis¶
explicit Chassis(const motion::MotionDeps& deps, motion::ITickPacer& pacer, const ChassisConfig& config = {})
deps is the same validated bundle every motion takes; pacer is the seam through which the world advances during blocking verbs (host sim: step the plant; robot: delay to the tick boundary — R1/R3 build that one). All deps pointees AND the pacer must outlive the Chassis; the facade borrows, it does not own (header: construction).
function, declared at include/shulib/chassis/chassis.hpp:244.
Chassis::Chassis (overload 2)¶
Neither copyable nor movable: the Chassis OWNS the scheduler, which is pinned in place by its own self-referential command-id stamp, so a copy or a move would leave that stamp pointing at the wrong object. Hold a Chassis&; construct it once, where it will live.
function, declared at include/shulib/chassis/chassis.hpp:254.
Chassis::Chassis (overload 3)¶
Neither copyable nor movable: the Chassis OWNS the scheduler, which is pinned in place by its own self-referential command-id stamp, so a copy or a move would leave that stamp pointing at the wrong object. Hold a Chassis&; construct it once, where it will live.
function, declared at include/shulib/chassis/chassis.hpp:255.
Chassis::operator=¶
Neither copyable nor movable: the Chassis OWNS the scheduler, which is pinned in place by its own self-referential command-id stamp, so a copy or a move would leave that stamp pointing at the wrong object. Hold a Chassis&; construct it once, where it will live.
function, declared at include/shulib/chassis/chassis.hpp:256.
Chassis::operator= (overload 2)¶
Neither copyable nor movable: the Chassis OWNS the scheduler, which is pinned in place by its own self-referential command-id stamp, so a copy or a move would leave that stamp pointing at the wrong object. Hold a Chassis&; construct it once, where it will live.
function, declared at include/shulib/chassis/chassis.hpp:257.
Chassis::~Chassis¶
Neither copyable nor movable: the Chassis OWNS the scheduler, which is pinned in place by its own self-referential command-id stamp, so a copy or a move would leave that stamp pointing at the wrong object. Hold a Chassis&; construct it once, where it will live.
function, declared at include/shulib/chassis/chassis.hpp:258.
Chassis::moveTo¶
Drive to target (FIELD pose): the decoupled holonomic engine — translation and rotation simultaneous and independent (C1's thesis).
function, declared at include/shulib/chassis/chassis.hpp:264.
Chassis::strafeTo¶
Translate to FIELD (x, y) while actively HOLDING the heading the robot has at its first live tick. On tank (authority 0) an off-line target honestly exits TimedOut (C1's drivetrain honesty).
function, declared at include/shulib/chassis/chassis.hpp:274.
Chassis::turnTo¶
Rotate in place to a FIELD heading, always the short way (F3's shortest signed error; exact ±180° resolves CCW, deterministically).
function, declared at include/shulib/chassis/chassis.hpp:284.
Chassis::followTrajectory¶
TrajectoryResult followTrajectory(std::span<const math::Pose2d> waypoints, const MotionOptions& options = {})
Chain waypoints as sequential moveTo legs, settling at each; stop at the first non-Settled leg (header: followTrajectory). options apply PER LEG (each leg is one scheduled motion with its own watchdog). Precondition: at least one waypoint. G2 boundary in the header.
function, declared at include/shulib/chassis/chassis.hpp:295.
Chassis::followTrajectory (overload 2)¶
TrajectoryResult followTrajectory(std::initializer_list<math::Pose2d> waypoints, const MotionOptions& options = {})
Brace-list convenience: followTrajectory({a, b, c}).
function, declared at include/shulib/chassis/chassis.hpp:324.
Chassis::brake¶
Stop the drivetrain (0 V under Brake) and block until the ESTIMATE certifies rest (or the watchdog fires). The controlled end-of-motion stop; cancel() is the uncontrolled one.
function, declared at include/shulib/chassis/chassis.hpp:336.
Chassis::hold¶
Actively hold the pose the robot has at its first live tick for duration, driving back any disturbance with full holonomic authority; Settled iff still within tolerance when the window ends. duration must be finite and > 0 (HoldPose's precondition). Typed time (D2): hold(500_ms) — hold(500) does not compile, so "500 meaning milliseconds" cannot hold pose for 500 s of a 15 s auton.
function, declared at include/shulib/chassis/chassis.hpp:348.
Chassis::wait¶
Wait, commanding nothing, for duration — then return. The world keeps advancing and the active motion (if any) keeps ticking — the same contract as waitUntil; the drive keeps whatever state the last verb left it in (after a settled motion: stopped). Deliberately DISTINCT from hold(): wait() never energizes the drive — this is the "sit still for the alliance partner" beat (D2; adopted from D1's finding that the naive waitUntil(false-pred, t) spelling logs a spurious Warn on every deliberate pause, and the Warn-free spelling needed Tier-3 plumbing). Returns void: a wait has no failure mode — a pacer that stops advancing the clock trips the scheduler's loud precondition, a programming error rather than a verdict. Warn-free and bounded by construction: the deadline predicate is time-monotone, so the internal timeout backstop is unreachable slack. duration must be finite and > 0 (typed: wait(2_s) / wait(500_ms)).
function, declared at include/shulib/chassis/chassis.hpp:368.
Chassis::drive¶
Command a chassis velocity directly, in the frame the CALLER names (no default — header: drive). Pre-empts any active motion; owns one loop iteration (estimate update → shared pipeline → health → record). Precondition: all three components finite.
function, declared at include/shulib/chassis/chassis.hpp:385.
Chassis::cancel¶
Stop the active motion into the defined safe state (0 V + Brake); with no active motion this is the PANIC STOP and still safes the drive.
function, declared at include/shulib/chassis/chassis.hpp:429.
Chassis::waitUntil¶
template <typename Pred> [[nodiscard]] motion::WaitResult waitUntil(Pred&& pred, units::Time timeout)
Block until pred() holds or timeout elapses (required, finite, >= 0; 0 = an honest poll) — the return says which. The active motion (if any) keeps ticking throughout; the world keeps advancing. Timing out logs one Warn and raises NO fault (a timed-out wait is a strategy branch, not a pathology). C2's verb, re-exported with typed time at the public edge (D2); the scheduler's own seconds-double signature is interior, per F3's internal-seconds convention.
function, declared at include/shulib/chassis/chassis.hpp:438.
Chassis::pose¶
The current fused FIELD pose estimate.
function, declared at include/shulib/chassis/chassis.hpp:446.
Chassis::setPose¶
Seed / teleport the estimated POSITION (x, y) — heading stays IMU-owned (the Localizer's structural choice). Call at auton start with the measured starting pose.
function, declared at include/shulib/chassis/chassis.hpp:451.
Chassis::strafeAuthority¶
Read-only passthrough of the drivetrain's sustainable lateral authority (fraction of the linear budget; F5). Routine authors budgeting lateral legs legitimately want it — the difference between a 2 s and a 3 s leg on the H-bot (C3 §11 #2, adopted).
function, declared at include/shulib/chassis/chassis.hpp:457.
Chassis::lastExitReason¶
Exit reason of the most recently finished motion (Settled on a virgin chassis — completedCount() via scheduler() says whether anything ran).
function, declared at include/shulib/chassis/chassis.hpp:463.
Chassis::lastCompleted¶
The most recent motion boundary — id/name/exit/abortFault/times (C5's raw material; abortFault names a fault-policy cause).
function, declared at include/shulib/chassis/chassis.hpp:469.
Chassis::motionConfig¶
The config the verbs run under (per-call options override per motion).
function, declared at include/shulib/chassis/chassis.hpp:474.
Chassis::deps¶
The STAMPED deps bundle — build custom IMotions from THIS and their records carry command ids like the built-in verbs' do.
function, declared at include/shulib/chassis/chassis.hpp:480.
Chassis::scheduler¶
The owned scheduler, for async composition / caller-paced tick() / counters. It is the SAME single motion slot the verbs use: async() here pre-empts a facade verb's motion and vice versa (one-active- motion is structural, never relaxed).
function, declared at include/shulib/chassis/chassis.hpp:486.
Chassis::scheduler (overload 2)¶
The same scheduler, read-only — for counters and last-motion state from a const Chassis&. Identical object and identical semantics to the non-const overload; the two differ only in what they let you do.
function, declared at include/shulib/chassis/chassis.hpp:490.
Design commentary, from the header¶
The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.
Chassis — the public facade every auton is written against (chunk C4, WS6/M2).
═══ STATUS: FROZEN — F6, LOCKED 2026-08-12 (chunk D2, API 2.0) ═══════════════════
This surface is FROZEN. Every public member below — and the three public
types ChassisConfig / MotionOptions / TrajectoryResult — changes only with
a major API-version bump plus a migration note (include/shulib/version.hpp
states the policy); additive extension (new verbs, new MotionOptions
fields, new overloads) stays legal and is the intended growth path. The
freeze is enforced STRUCTURALLY: test/f6_signature_pin_test.cpp asserts
every frozen member's exact type at compile time and fails the build,
naming F6, if one drifts. NOT inside F6, deliberately: Routine /
RoutineResult / RoutineStopCause (routine.hpp — they froze at D3 as their
OWN register row F10, because the recipe layer is a different tier and can
version independently of the facade; see routine.hpp's banner); the
scheduler's and deps bundle's OWN member
surfaces behind scheduler()/deps() (those belong to C1/C2's layers); and
the lower-layer config fields reached through ChassisConfig. The
candidate-era reasoning for each shape is in the C4 completion record and
the freeze rulings (what joined, what stayed out, and why) in the D2
completion record (development log, shulib-v2 branch).
═══ What this class is ══════════════════════════════════════════════════════════
The composition root of the MOTION stack: it owns the MotionScheduler and
wraps C1's primitives in blocking verbs. It DELEGATES everywhere — motion
logic is C1's, scheduling/fault policy C2's, kinematics C3's, the command
choreography command_pipeline.hpp's. If a behaviour looks like it lives
here, its test and its fix belong in the layer that owns it.
verbs moveTo · strafeTo · turnTo · followTrajectory · drive(speeds, Frame)
brake · hold · wait (C4 candidates + the D2 addition, all in F6)
control cancel (panic stop) · waitUntil(pred, timeout)
state pose · setPose · strafeAuthority · lastExitReason · lastCompleted
Tier 3 scheduler() · deps() — the no-ceiling seam
═══ ONE deps source (C2's structural handoff, closed here) ══════════════════════
The facade constructs the scheduler from the caller's MotionDeps and EVERY
motion from scheduler.deps() — the stamped bundle whose telemetry routes
through the command-id stamp. At C2 that plumbing was a convention a caller
had to remember (its one named gap, §5 D10); through the facade there is no
unstamped path: a verb CANNOT build a motion from raw deps, so every record
of every facade motion carries its command id structurally. Tier-3 callers
composing their own IMotion get the same guarantee by building from
chassis.deps().
═══ Construction: the standalone promise (locked principle, master plan §16.2) ═══
A code-fluent team builds a working Chassis in PLAIN C++ — value-construct a
kinematics preset (xDrive / hDrive / TankKinematics: drivetrain is config
data), wire the HAL + Localizer + FaultLatch + HealthMonitor into a
MotionDeps, pick a pacer, done. No .vexbot file, no VexBuilder, no config
file of any kind is EVER required; G1's RobotBuilder.from(profile) is an
additional on-ramp, never the only path. (Pinned by the file-free
construction test.)
The facade BORROWS the deps (all pointees + the pacer must outlive it) and
OWNS the scheduler + its MotionConfig. It deliberately does NOT own the
Localizer or the HAL: wiring those is the builder's job (G1) or the
caller's, and owning them here would weld the facade to one localization
stack — Tier 3 must be able to swap estimators without losing the facade.
═══ API semantics the lower layers guarantee (carried, not re-implemented) ═══════
These are facade-level API behaviour — user code WILL depend on them:
* BLOCKING VERBS: each of moveTo/strafeTo/turnTo/followTrajectory/brake/
hold is async() + waitUntilSettled() on the owned scheduler — it returns
only when the motion exits, and CANNOT hang: the motion's own watchdog
bounds it (C1, mutation-proven), including any boot wait; a pacer that
stops advancing the clock trips a loud precondition (C2). The returned
ExitReason is the motion's honest verdict (Settled / TimedOut /
Cancelled — never Running). Deliberately NOT [[nodiscard]]: discarding
it is a legitimate auton style (the fault latch + C5 result lines carry
the pathology), and forcing (void) casts on every routine line would
punish the common case.
* WAIT-FOR-LIVE (C1): a verb issued during the boot window (estimate still
Uninitialized) WAITS, motionless, watchdog running — a never-live
estimate exits TimedOut rather than hanging. Budget timeouts to cover
IMU calibration (~2 s).
* PRE-EMPT (C2): starting a verb while a motion is active (possible via
the Tier-3 seam or a waitUntil predicate) cancels the old motion into
the safe state first; there is never a tick on which two motions
command. drive() pre-empts identically — a manual command supersedes.
* CANCEL SAFE STATE (C1/C2, HA-53): cancel() puts every drive motor at
0 V + BrakeMode::Brake synchronously; with no active motion it is the
PANIC STOP and still applies the safe state.
* FAULT POLICY (C2): a fault in the configured abortFaultMask raised
during a motion aborts it into the safe state; the verb returns
Cancelled and lastCompleted().abortFault names the cause. Default mask:
ODO_STUCK only (the estimate is lying). The run continues — faults log
and recover, they never crash.
* TURN-WHILE-DRIVE ON LIMITED-STRAFE DRIVES (C3): on an H-drive, a
lateral-dominant leg runs authority-limited — translation proceeds at
the achievable |vy| while vx and ω stay at full authority; rotation is
never sequenced before translation. The mode is telemetry-visible
(record strafeFallbackActive → TermSink " SFB"), never silent. There is
deliberately NO polling getter for it: a live-polled bool invites
control-flow coupling to a telemetry concept (C3 §11's recommendation,
adopted). On tank, laterally-offset targets honestly exit TimedOut.
═══ drive(ChassisSpeeds, Frame) — the frame-explicit manual verb ═════════════════
The verb a driver-control loop calls every iteration (field-centric or
body-frame driving), and the escape hatch for direct velocity control in
auton. The Frame parameter has NO default: the caller must say which frame
the command is in, so silent frame confusion — the classic bug class this
rebuild exists to prevent — is a compile error.
* It pre-empts any active motion (above), then runs ONE loop iteration it
owns: localizer.update() FIRST (a teleop loop that never advanced the
estimate would rot the field rotation into exactly the frame bug),
then the shared command pipeline, health observables, one record.
Intended use: call it at your loop cadence; between calls nothing else
needs ticking.
* Frame::Field during the boot window commands ZERO volts (+ one Warn per
window): a field-relative command needs a heading, and the boot estimate
does not have one — rotating by garbage would move the robot in a
garbage direction. Frame::Body works during boot (no estimate needed).
* No stall cross-check: the driver is the supervisor in teleop, and there
is no target to cross-check against. Health observables still tick, so
IMU_LOST / BROWNOUT / OVER_TEMP stay live. Records carry command id 0
(no scheduled motion) — the honest attribution.
═══ followTrajectory — the shape is F6, the body is deliberately minimal ═════════
Chains the waypoints as sequential MoveToPose legs through the scheduler,
settling at each (stop-and-settle is v1's documented motion model; blending
is a measured Frontier item). Stops at the FIRST non-Settled leg and
reports it — a robot that timed out mid-trajectory is lost, and chasing
later waypoints on a lie compounds blindly.
G2 BOUNDARY, stated honestly: no marker callbacks, no command ids on
waypoints, no .vexbot ingestion, no profiled/curved segments — those are
G2's PathRunner, built on this same scheduler's waitUntil primitive. This
verb exists now because F6 freezes the VERB SET; a richer Trajectory type
arrives as an ADDITIVE overload, never a reshape of this one.
═══ Options (per-call, additive-extensible) ══════════════════════════════════════
MotionOptions carries the per-call knobs every real auton needs (a slow
precise approach leg is table stakes). 0 means "use the config default".
An options STRUCT (not positional parameters) is deliberate: post-freeze,
new knobs are added fields — no signature change, no migration.
Single-task by contract, like everything it composes. Not copyable/movable
(it owns the scheduler, which is pinned by its self-referential stamp).
Last updated 2026-08-14 — published from the main release branch, commit 6d5dd35. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.