shulib v2 — Master Plan & Architecture Reference¶
Status: the library is built and host-verified; this document is its architecture reference, not a forward plan. Most of what follows is now shipped code — where a section still describes something unbuilt, it says so in place. Living document.
The one status line that governs everything: shulib has never driven a robot. No control loop has ever closed on hardware, no wheel has ever turned under the library's own steering. What has happened: the code boots on a V5 brain, and the hardware adapters have commanded real motors and read real sensors at a bench, proving their unit conversions correct on one robot, once. Everything above that seam is verified against a simulator only.
What this is: a design & architecture reference for shulib — the reasoning behind the shape, including the alternatives that were rejected.
(This header read "DESIGN-FIRST (no code yet) … sections marked PENDING … Last updated 2026-06" until 2026-08-14. All three clauses were false: the library was built, no
_PENDING_marker existed anywhere in the file, and the file already carried notes dated two months later. It is the first thing a public reader sees, which is what made it worth this much correction.)
Table of Contents¶
- Mission & Thesis
- Context & Constraints
- Locked Decisions
- Why shulib Beats LemLib & OkapiLib
- Architecture (Layered)
- Module Map
- Canonical Conventions (frame, units, accuracy targets)
- Subsystem Designs
- Roadmap (clean-room build) — full breakdown in roadmap.md
- The Onboard-Estimation Story & Award Narrative
- Red-Team Gates & v1 Cut-List
- Accuracy Risks to Watch
- Open Decisions / TBD
- Skills Strategy & Routing
- The One-Stop-Shop Capability Catalog (past · present · future)
- VexBuilder Integration Contract
- Accessibility & Progressive Disclosure (non-coder teams)
- Diagnostics & Observability
- Glossary & References
1. Mission & Thesis¶
Rework shulib (PROS / VEX V5 / C++20) into a motion + autonomy library that is genuinely
better than LemLib for this team's holonomic robots, delivering extremely accurate
autonomous for the VEX U Autonomous Coding Skills Challenge (Override, 2026-27).
Thesis. LemLib is structurally tank/differential-only — every motion ends in
desaturate(lateral, angular) → left/right wheels, a model that cannot strafe and therefore
cannot drive a 24" X-drive or 15" H-drive — and its odometry is tracking-wheel + IMU
dead-reckoning with no absolute correction, so error grows unbounded over a 60s run.
shulib v2 inverts both:
- One
IKinematicsabstraction makes the universal motion currency a field-frame twist(vx, vy, ω), so the same motion code drives X-drive / H-drive / tank, and translation is decoupled from heading (strafe into a goal while facing an AprilTag — impossible on a differential drive). - A fused
Localizer(Pilons arc odom + IMU-owned heading + V5 GPS + AprilTag, Mahalanobis-gated) bounds drift across the full minute, exploiting the GPS code strip that rule<RSC4>guarantees on the Auto-Skills field. - All atop a PROS-free, dependency-inverted Hardware Abstraction Layer, so the core is host-unit-testable and VexBuilder-sim-runnable — neither of which LemLib is — with profiled + kS/kV/kA feedforward control for speed-independent accuracy.
2. Context & Constraints¶
- Division: VEX U (VURC). Two robots per team, used together (incl. Skills,
<VURS2>): - 24" robot — X-drive (4 omni @ 45°, full holonomic).
- 15" robot — H-drive (tank + transverse strafe wheel; limited strafe authority).
- Sensors (full stack, both robots): V5 IMU, V5 Rotation sensors w/ dedicated tracking
wheels, V5 GPS, AI Vision, Optical, Distance. VEX U also legalizes onboard compute
(Raspberry Pi / Coral,
<VUR12>) and LIDAR/spinning sensors (<VUG3>). - Game / Skills scoring (Override): red/blue Pin = 5; yellow Pin = 10 if Owned (quadrant Toggle set to your color, or robot ends in the Midfield for Midfield yellows); Robot in the Midfield at end = 8. Objects: Pins (two colored halves) and Cups (transparent + opaque) that nest into stacks on Goals. 4 Toggles (one/quadrant). Match-loads intaken from Loaders. Auto Coding Skills = 1:00 fully autonomous.
- VEX U skills match loads (
<VURS1>): 4 red/yellow Pins, 6 blue/yellow Pins, 10 Cups. - Ecosystem: shulib is the runtime half of a two-tool ecosystem. VexBuilder (a separate,
in-progress app) is the single authoring tool — robot design, path planning, and a (planned) sim,
all saved in one
.vexbotproject file. shulib must be standalone-usable; we design only the seams (HAL, the.vexbotconfig+paths it reads,SHUL/2telemetry). No authoring/sim inside shulib. - Build window: pre-season, ~2–3 months dedicated build time → clean-room v2 build (no legacy auton to protect; replace, don't patch).
3. Locked Decisions¶
| Decision | Choice |
|---|---|
| Build scope | Tier it — full ambition is the north star, proven subset ships first (complementary filter → EKF; trapezoid → S-curve; GPS-only → +AprilTag). |
| Vision backend | Both — a VEX-U Raspberry Pi (student CV) and the V5 AI Vision Sensor, behind one ITagSource (run both, cross-check, fuse). |
| Migration | Clean-room v2 build over the pre-season window. No parallel-legacy adapters required. |
| Multi-robot coordination | Stretch goal — thin seam only; prove the core on the X-drive first. Running shulib on both bots is just two independent instances, which is core (not "coordination"). |
| Odometry config | Two perpendicular unpowered tracking wheels + IMU-as-heading + GPS/AprilTag correction (X-bot); L/R + transverse + IMU on the H-bot (wheel-heading = cross-check only). |
4. Why shulib Beats LemLib & OkapiLib¶
We benchmark against two libraries, for different reasons. LemLib is the capability baseline (modern, popular, tank-only — the bar to clear on motion/localization). OkapiLib is the style baseline (the cleaner-architected ancestor — the bar to clear on ergonomics). shulib must beat both. The table below is vs LemLib; §4a is the OkapiLib pass.
| Dimension | LemLib | shulib v2 |
|---|---|---|
| Drivetrain | Tank/differential ONLY; cannot strafe ("not going to change until other drivetrains are competitive"). | IKinematics twist (vx,vy,ω) → per-wheel cmds + inverse. XDrive / HDrive (capped strafe) / Tank subclasses. New drivetrain = new subclass, zero controller edits. |
| Localization | Tracking wheels + IMU dead-reckoning. No GPS/vision/fusion. Drift unbounded over a run. | Fused Localizer: Pilons odom + IMU-owned heading + GpsCorrector + AprilTagCorrector behind IPoseSource; complementary→EKF on SE(2) with Mahalanobis gating. Bounded over 60s; degrades gracefully. |
| Motion | Boomerang moveToPose, pure-pursuit follow — heading coupled to travel direction. |
Decoupled per-axis (x,y,θ): profiled vx/vy to target while independently servoing θ. MoveToPose / TurnTo / StrafeTo / FollowTrajectory (independent per-waypoint heading). |
| Architecture | PROS objects embedded throughout; global namespaces; not unit-testable or sim-able. | 5-layer dependency-inverted; core depends ONLY on a PROS-free HAL. Host doctest suite + sim adapters. CI forbids PROS includes in the core in both spellings (<pros/…> and "pros/…"), exempting the adapter tree by exact path. |
| Units | Migrating mid-library to a heavy type-safe lib; API churn. | Lightweight header-only Quantity<Dim> (~6 dims) + _in/_deg/_tile literals + built-in angle wrap. Kills the deg-into-cos/sin bug class at compile time. |
| Async | Global MotionHandler; single motion. |
MotionScheduler object (injected clock/HAL): one active IMotion, .async()/waitUntilSettled()/waitUntil(pred)/cancel(); min-velocity handoff chaining. |
| Accuracy | PID-only (derivative-on-error, no FF), degrades with speed. | Bounded error via absolute correction; profiled + kS/kV/kA FF + derivative-on-measurement PID w/ integral clamp; vision-servo docking decouples sub-inch scoring from drift. |
| Ecosystem | Monolith; no seams. | Two boundary contracts: the .vexbot config+paths sub-schemas shulib reads + SHUL/2 telemetry (plus the internal HAL sim/real/test seam). Fully standalone — pure-code use needs no file at all. |
4a. The OkapiLib pass (style baseline)¶
OkapiLib is the cleaner-architected ancestor — in several places it is closer to our target than LemLib is. We treat it as a positive model to learn from, not just a target to beat. Two facts frame it:
- Archived / frozen. Read-only since 2023-02-03; last release v4.8.0 (2022-06). No Override support, no active development. Building on it is a non-starter; beating a stationary target is the easy half.
- Better style, same capability gaps. Its odometry is wheel-encoder-only (no IMU/GPS/vision
fusion — same core gap as LemLib), and its motion is turn-then-drive decomposition:
driveToPoint/turnToPointcompute an angle + distance and forward to a tank-styleChassisController. Even on a 3-wheel X-drive it does not strafe-to-pose under closed loop — it tracks omnidirectionally but moves like a tank. Its frame is +x fwd / +y right (opposite handedness from our §7 +X/CCW — a third independent reason to freeze the frame before any code).
Adopt from OkapiLib (steal the good ergonomics, with credit):
- Dimensional-analysis units — OkapiLib's
QLength/QAngle/QTimeproves the type-safe-units bet pays off. Validates ourQuantity<Dim>decision (§7, §13 #3); keep it lighter (~6 dims, no template-heavyRQuantityverbosity / compile-time blowup). SettledUtil-style settling — exit on (|error| < tol) and (|derivative| < tol) and (held fortimems). This is the principled replacement for the oldwhile(fabs(error)>1)hang bug; bake it into everyIMotionexit condition + the motion watchdog (§12).- Fluent builder —
ChassisControllerBuilderergonomics → ourChassisbuilder (§8) already follows this; hold the bar there. - Composable filters —
EmaFilter/MedianFilteras small objects → afilters/utility for conditioning of task-sensor / alignment signals (Distance/Optical/vision-offset fordockToGoal) — not the EKF measurement inputs (GPS/AprilTag/odom), which are conditioned only by the gated/adaptive-R fusion math (§13 #4), never by EMA/median. AsyncController+ControllerRunner— formalize ourMotionScheduler(§4 Async row) against this proven interface shape.- Squiggles (their path generator, replaced Pathfinder) — reference prior art for the VexBuilder path planner, not a shulib runtime dependency.
Where shulib is strictly better than OkapiLib: maintained & game-current · true holonomic motion-to-pose (twist currency) vs turn-then-drive · multi-sensor fusion vs wheel-only · host-unit-testable core vs PROS-bound · defined ecosystem seams.
5. Architecture (Layered)¶
Dependencies point down / inward only — no layer includes a header from a layer above it.
L4 APPLICATION (per-robot, student-owned)
src/main.cpp: initialize / autonomous / opcontrol
Composition root: builds HAL impls -> RobotContext -> Chassis
| depends on
L3 FACADE
Chassis (orchestration) RobotContext (DI container of HAL handles)
Verbs: moveTo / strafeTo / turnTo / followTrajectory / drive(ChassisSpeeds, Frame)
|
L2 CORE (pure C++20, NO pros, host-testable)
motion/ localization/ control/
IMotion IPoseSource Pid (d-on-measurement)
MoveToPose/TurnTo Localizer (fused) Feedforward {kS,kV,kA}
StrafeTo/Follow PilonsOdometry MotionProfile (trap/S-curve)
MotionScheduler Gps/AprilTagCorrector ExitCondition / ExitGroup
|
L1 KINEMATICS + MATH (pure C++20)
kinematics/ IKinematics, XDriveKinematics, HDriveKinematics, TankKinematics
math/ Pose2d, Twist2d, ChassisSpeeds, Angle (wrap), units::Quantity<Dim>
|
L0 HAL INTERFACES (pure abstractions, ZERO pros)
hal/ IMotor IRotation IImu IGps IVision IDistance IOptical IClock ITelemetrySink
| implemented by (NOT depended on by core)
hal/pros/* (real V5 — the ONLY files that may include PROS, and they must
use the QUOTED form "pros/…": the robot build resolves includes
with -iquote only, so <pros/…> compiles everywhere except the
one build that matters)
hal/sim/* (VexBuilder) hal/fake/* (host doctest doubles)
Data flow (per ~10ms tick): HAL sensors → Localizer.update() → fused Pose2d/Twist2d →
ActiveMotion.update(dt, pose, vel) (profile FF + per-axis x/y/θ PID + ExitGroup) →
ChassisSpeeds (FIELD frame) → Chassis rotates FIELD→BODY (the one correct transform) →
IKinematics.toWheels() → desaturate → IMotor::setVoltage() → Telemetry.emit(...) → SHUL/2.
6. Module Map¶
L0 HAL — hal/ pure interfaces (IMotor, IRotation, IImu, IGps, IVision,
IDistance, IOptical, IClock, ITelemetrySink); hal/pros/ V5 adapters (only files that
include <pros/*>); hal/sim/ (VexBuilder) + hal/fake/ (deterministic test doubles).
L1 Math/Kinematics — math/ (Pose2d, Twist2d, ChassisSpeeds, Angle,
units::Quantity<Dim>, literals); kinematics/ (IKinematics + XDrive/HDrive/Tank,
forward + inverse + desaturate + strafeAuthority()).
L2 Core
- control/ — Pid (derivative-on-measurement, integral + output clamp, injected clock),
Feedforward{kS,kV,kA}, MotionProfile (trapezoid; S-curve later), ExitCondition/ExitGroup.
- localization/ — PilonsOdometry (kept arc math, mid-heading, diagnostics off hot path);
Localizer (fuses odom + IMU heading + correctors; complementary → EKF; quality flag);
GpsCorrector + AprilTagCorrector behind IPoseSource/ICorrector.
- motion/ — IMotion, MoveToPose, TurnTo, StrafeTo, FollowTrajectory,
MotionScheduler (async/cancel/chaining).
L3 Facade — chassis/Chassis + RobotContext (composition root; the only object that
differs across hardware/sim/test).
Skills layer — alignment/ (DockToGoal visual-servo, Distance fallback),
manipulation/ (IntakeFromLoader, PlacePin, NestCup, SetQuadrantToggle),
sequence/ (Action engine: Sequence/Parallel/Race/Deadline + match-timer park guard).
See §14 for the full skills-layer primitive set (synthesized from the strategy digest).
Status note (F1, 2026-08-13): manipulation/ now exists in the tree, holding the season-free
layer the primitives above will be built from — the operation contract (IMechanismOp), two
generic bounded operations (RunUntilConfirmed, ActuateAndConfirm) and the stall detector.
That contract layer depends only on hal/ + control/ + diag/, so it sits BESIDE motion/
(L2), not above the facade; the concrete scoring primitives listed above (F3) are what may
consume facade verbs and sit at skills level. The device seam itself (IMechanism,
MotorMechanism, PneumaticMechanism, IDigitalOut) lives in hal/ — outside the F4 freeze,
register row F11.
IO / Tools — io/Trajectory + .vexbot paths[] reader (+ legacy .shupaths importer); io/Telemetry SHUL/2
(ports/absorbs logger.hpp into the new core before the end-of-M2 legacy deletion, fixing its 3 known bugs — §18.6); tools/sysid (offline kS/kV/kA least-squares → emits constants,
not code) + on-robot tuner.
7. Canonical Conventions¶
Coordinate frame is artifact #1 — the #1 silent accuracy-killer. Freeze before any code.
RATIFIED 2026-06-08 (LOCKED — +X / CCW):
- Origin (0,0) at field center; +X right, +Y away from the red station (audience view).
- Heading θ = 0 along +X, increasing CCW (standard math convention).
- Internal units: inches + radians + seconds, always. Degrees only at the user-API boundary
(_deg); milliseconds only where a clock hands them over, converted to seconds at the HAL edge.
(Time unit = seconds, locked 2026-06-19 — matches kS/kV/kA feedforward; ms-into-seconds is its own
drift bug-class, killed the same way as deg-into-trig.)
- The V5 IMU (compass/CW) and GPS (own frame) are converted to canonical exactly once,
at the HAL adapter. One unit-tested fieldToRobot() / robotToField() pair is the only
place a frame transform is allowed. A single Rotation2d/Angle type owns wrapping → kills the
deg-into-cos/sin bug class.
- Rejected alternative: "0 = forward/+Y, CW" (the old odom). Valid in isolation, but we go
+X/CCW — standard math convention, matches the kinematics / EKF literature.
Accuracy targets (RATIFIED 2026-06-08 — heading is a HARD requirement; validate on the field):
- Heading: < 1.0° error, always. This is the firm team spec; position targets flex around it.
- v1 (GPS + wheels + IMU, no vision docking): end-of-60s pose within ~1.0″ and < 1.0°;
run-to-run repeatability within ~0.75″.
- With vision docking (Phase 3): final goal alignment within ~0.25″ and < 1.0° closed-loop
(drift-independent; typically < 0.5° on a tag) — enough to nest a 1.6″ Pin with a funnel for the residual.
- Each becomes a telemetry-based pass/fail acceptance test per phase.
- Engineering consequence (honest): a raw V5 IMU yaw drifts ≈ 1°/min, so dead-reckoned heading
alone will not reliably hold < 1° across a 60s run. The spec therefore makes IMU-owned heading +
absolute yaw correction (AprilTag/GPS) load-bearing, not optional — Phase 3 yaw correction is
promoted from "nice-to-have" to required to meet spec. [[vexu-override-facts]]
IMU → canonical binding contract (set by the 2026-06-19 conversion red-team; the pure
conversion is in hal/imu_conversion.hpp, enforced when the hal/pros adapter lands):
- Bind heading to pros::Imu::get_rotation() (cumulative, unbounded) — not get_heading()
(bounded [0,360), loses revolution continuity). Both are CW-positive per the PROS headers.
- The adapter performs no tare/set_*/reset after calibration; any of those re-zero the
sensor independently of bootHeading and silently break the offset (the < 1° budget can't
absorb a static misalignment).
- bootHeading has one owner: the robot's canonical start heading (the init Pose2d given to
the Localizer). The offset is applied exactly once at the HAL edge, never re-applied downstream.
(Formally locked with the hal/pros adapter at M1 / Localizer at M2.)
- Yaw rate has no CW-positive PROS scalar — source it by differentiating get_rotation(), or
bench-verify the undocumented sign of get_gyro_rate().z.
GPS → canonical binding contract (set by the 2026-06-19 conversion red-team; pure conversion in
hal/gps_conversion.hpp):
- Position axes (VEX +X = East, +Y = North) are ASSUMED, unverified — PROS does not document them.
A wrong axis label MIRRORS the pose, and northHeadingDeg (a rotation) cannot fix a reflection.
Bench-measure the raw→wall mapping (field-cal oracle test) before a scored run.
- northHeadingDeg (canonical heading of VEX-North; default 90° = away from red, else 270/−90°)
and the lever arm (BODY forward/left inches) each have one owner = the robot start pose (same
authority as the IMU bootHeading), applied exactly once at the HAL edge.
- The adapter constructs pros::Gps port-only and never sets a firmware offset
(set_offset/initialize_full/offset ctors); otherwise get_position() reports the center and the
lever arm is double-subtracted (inches of silent bias). Boot-check get_offset() == (0,0).
- The adapter screens PROS_ERR_F (off-strip/failed) → hasFix()=false before converting (§13 #4;
Driving Skills has no strip), and scales get_error() meters→inches for rmsError().
HAL interface conventions & F4 scope (settled by the 2026-06-19 F4 freeze review):
- Health/validity signaling, two tiers: a POSITIVE boolean usable-gate for hard gates
(IGps::hasFix, IImu::isReady — true = trustworthy, never inverted), and a [0,1] confidence()
for graded trust (IDistance, vision). Sensors with no validity signal (IClock/IBattery/IMotor/
IRotation/IOptical) are always-valid; per-sensor isConnected() is a deliberate M2 additive
(ships with the fault-code enum).
- Units: every physical quantity is units::Quantity (now incl. a 5th electric-current dimension
→ Current amps, Power = V·A); every heading is math::Angle; normalized [0,1] fractions
(confidence/saturation/brightness/proximity/capacity) and the color-hue degrees value are bare
double by design (dimensionless or non-spatial), as is motor temperature (°C, never combined).
- Finiteness: every numeric HAL reader returns a finite, in-range value; the hal/pros adapter
clamps/normalizes vendor sentinels (PROS_ERR/PROS_ERR_F) at the edge, so no NaN/Inf reaches the core.
- F4 scope = sense + drive-motor + clock + telemetry. Deliberately deferred additive extensions
(safe post-freeze, "frozen" ≠ "complete"): isConnected() (M2); IImu linear acceleration (Frontier
EKF). The config-ingestion seam (IRobotConfig/IRouteSource, decision #10) is authored at M5
with its RobotConfig/Route schema (F7/F8), not part of this runtime-HAL freeze.
The first of the deferred additives has since landed: pneumatic/digital-out actuation
(IDigitalOut) and the Mechanism seam (IMechanism + MotorMechanism /
PneumaticMechanism) shipped at chunk F1 (2026-08-13) — as F4 siblings, explicitly outside the
F4 freeze (the locked row is untouched); they are register row F11, not frozen, freezing
after their second consumer (F3).
Odometry → canonical binding contract (set 2026-06-19 when WS5 re-derived the Pilons arc math
from scratch; the integrator is in localization/arc_step.hpp):
- One integration primitive. Per tick, arcStep is the exact SE(2) constant-twist exponential
map: scale the tracking center's BODY travel (forward, lateral) by the chord factor
k = 2·sin(Δθ/2)/Δθ and rotate by the AVERAGE heading θ₀ + Δθ/2. k → 1 as Δθ → 0
(straight-line limit); the only guarded singularity is exactly Δθ == 0 (0/0).
- Rotate by the AVERAGE heading — never the new or the start heading. The legacy lodge odom
rotated the chord by the new heading (θ₀ + Δθ), a systematic +Δθ/2 per-tick bias: a 90° arc
lands a (10,10) move at (0, 14.14). That alone would blow the < 1°/sub-inch budget. Re-derived
clean so the bug is not inherited; pinned by an exact-endpoint test + 6 killed mutations.
- Heading is IMU-owned. arcStep takes Δθ from the two IMU heading samples (tick start/end) via
Angle::errorTo (shortest signed, wrap-correct); it never derives heading from the wheel
difference (that is the H-bot's cross-check only, decision #3). Per-tick Δθ at ~100 Hz is tiny, so
shortest-arc == actual rotation — arcStep assumes (and documents) a small per-tick rotation.
- Offsets applied once, by the odometry class, before arcStep. Each tracking wheel's reading is
converted to tracking-center travel by removing the rotation-induced component (Δθ × offset) at the
odom edge; arcStep itself is offset-agnostic and works in center coordinates, so the geometry stays
pure and reusable (a kinematics-integrated holonomic odometry feeds the same primitive). A pure
in-place rotation must yield zero center translation (the offset signs are opposite — +Δθ·offset
forward, −Δθ·offset lateral — pinned by CW and CCW pure-rotation tests).
- Tracking-wheel binding (TrackingWheel). Travel = shaft_rad × (diameter/2). The hal/pros
adapter MUST source IRotation::position() from the cumulative reading (pros::Rotation::get_position),
not a wrapping 0–360 angle — the same get_rotation-vs-get_heading distinction the IMU has. The int32
centidegree range is ~6×10⁴ revolutions (≫ a match), so no wrap is seen in practice. Each wheel is built
by a role-stamped factory (forward() = +left offset, lateral() = +forward offset); PilonsOdometry
checks the roles, so a wrong-axis offset or a swapped pair fails at construction, not silently on the field.
- Trust gate + finiteness. lastDeltaImplausible() flags a tick whose |Δθ| > maxTickRotation (a
magnitude bound on the wrapped Δθ — it catches an alias only in (bound, π]; a real >π rotation that
aliases small is excluded by the loop-rate assumption, not the gate) or whose integration is
non-finite. A non-finite tick freezes the position (never writes NaN into the persistent pose); an
oversized-but-finite tick is flagged yet still integrated. Recovery beyond that is the fusion layer's.
(Hardened by the 2026-06-19 localization red-team: 5 lenses, every finding adversarially re-verified.)
8. Subsystem Designs (summary)¶
Sensor utilization map¶
Every sensor flows through the HAL into one fused estimate — none is trusted alone.
| Sensor | Bucket | How shulib uses it | Module |
|---|---|---|---|
| V5 IMU | localize | Authoritative heading (yaw) in the fused estimate; yaw-rate gates GPS; pitch/roll → tip detection; cold-calibrated at boot | localization/, safety |
| Rotation sensors (tracking wheels) | localize | Two perpendicular unpowered wheels → Pilons arc x/y; the predict backbone between absolute fixes | localization/PilonsOdometry |
| V5 GPS | localize | Absolute (x,y,θ) on the <RSC4> strip; gated corrector (adaptive R from get_error(), lever-arm + latency, Mahalanobis); dead-reckon-only off-strip |
localization/GpsCorrector |
| AI Vision + AprilTags | localize + task | relocalize() feeds a high-confidence absolute fix from a known goal tag into the gated corrector (low-R → fast, drift-canceling nudge, still innovation-bounded per §13 #4 — never a snap); dockToGoal() sub-inch visual-servo; pin/cup detect + color |
localization/AprilTagCorrector, alignment/, manipulation/ |
| Raspberry Pi / Coral (VEX U) | localize | Student CV coprocessor (AprilTag/object) → pose fixes to the Brain over serial; backs AI Vision behind ITagSource |
HAL ITagSource |
| Optical | task | setQuadrantToggle color confirm (no over-rotation); orientToScoringHalf (visible-half color); intake color/presence |
manipulation/ |
| Distance | task | dockToGoal no-tag fallback (v1); capture confirm; wall cross-check; goal-base-occupied → height-adaptive fallback |
alignment/, manipulation/ |
| Motor encoders + current (built-in) | task | liftToLevel height (encoder + linkage map + sag comp); drive velocity for FF/PID; current-spike capture/dock confirm + stall homing; thermal monitor |
control/, manipulation/ |
| Battery voltage | control | Scale motor-voltage commands by measured V so feedforward holds as the pack sags <11V | control/ |
| LIDAR (VEX U, optional) | localize (stretch) | 2D scan-match as another ICorrector; obstacle/robot detect |
stretch |
Two buckets: localization sensors (IMU + wheels + GPS + AprilTag/Pi [+LIDAR]) fuse into one gated pose that degrades gracefully as inputs drop out; task sensors (Optical + Distance + encoders/current + Vision) confirm every manipulation/alignment action so a no-driver auton never advances on a failed grab or place. Redundancy is the point — it's what LemLib (wheels + IMU only) structurally can't match.
- Localization / fusion. High-rate prediction (Pilons arc odom @ ~100 Hz, IMU-owned heading)
- low-rate absolute correction (GPS via
<RSC4>strip with adaptive R fromget_error(), lever-arm + latency comp; AprilTag PnP from tags 0–4). Error-state complementary filter first, upgraded to a 5-state SE(2) EKF[px,py,θ,vx,vy]with Mahalanobis gating, high-yaw-rate rejection, consecutive-reject re-init, and a hard dead-reckon-only flag (Driving Skills has no strip). Process noise scales with travel (slip ∝ distance). - Holonomic motion. Field-frame trapezoidal profile + kS/kV/kA feedforward +
derivative-on-measurement PID per axis; wheel-command desaturation (uniform scale).
MoveToPosedecouples translate + heading;StrafeTois the holonomic-only verb; H-drive caps commandedvytostrafeAuthority()with an automatic turn-then-drive fallback (observable in telemetry). - API & units. Fluent builder over HAL + kinematics + sensors + estimator; chainable async
motions; type-safe
Quantity<Dim>. Standalone autons need no planner. - Ecosystem seams. (a) HAL sim/real/test seam; (b) the
.vexbotpaths[]sub-schema (typed waypoints w/ independent per-waypoint heading + event markers) consumed byPathRunner/FollowTrajectory; (c)SHUL/2versioned, sequenced, typed telemetry stream behind a Sink (NullSink default = zero cost) for VexBuilder visualize/record/replay. All version-gated from day one; shulib fully functional with no VexBuilder present.
9. Roadmap (clean-room build, ~2–3 months)¶
Build in testable vertical slices: each phase ships a host-tested, hardware-validated unit. The in-depth, website-facing roadmap is
roadmap.md— full task breakdown per milestone (M0–M8), a Freeze Register of don't-break contracts, the 12 permanent workstreams, and the build/CI/test-harness scaffolding tasks. The phases below are the summary;roadmap.mdis the exhaustive source of truth for what's left to do.
- Phase 0 — Canonical foundation. Freeze the coordinate frame (unit-tested) + accuracy
targets; stand up
math/units, theAngletype, and the IMU-owned heading path. Establish that the old bug classes (field-centric overwrite, deg-into-trig, nullptr IMU, derivative-on-error) are structurally impossible by design. Milestone: heading stable on a 60s strafe test; frame transform unit-tested. - Phase 1 — HAL + kinematics foundation. HAL interfaces +
hal/pros+hal/fake;IKinematics+XDriveKinematics(+ Tank) host-tested with pure numbers; CI forbids<pros/>in core; freeze v1.vexbotpaths[]schema (theDebugRecord/SHUL/2schema freezes later — at M2 and M6 respectively, §18). Milestone: kinematics + a trivial motion run identically in a host doctest case and on the V5 by swapping onlyRobotContext. - Phase 2 — Real holonomic motion.
control/(trapezoid profile + FF + ExitGroup);motion/(MoveToPose/TurnTo/StrafeTo+MotionScheduler);Localizeron odom + IMU (correctors stubbed);HDriveKinematicsw/ capped strafe;tools/sysidkV/kA characterization. Milestone: a hand-written X-drive auton chains profiled motions; same code runs the H-bot. - Phase 3 — Bound the 60s drift (accuracy edge).
GpsCorrector+AprilTagCorrector(Pi + V5 AI Vision backends) → upgradeLocalizerto the EKF;alignment/DockToGoalvisual-servo (Distance fallback); documented calibration routines. Milestone: pose error bounded across a full 60s run; docking nests a 1.6″ Pin repeatably. - Phase 4 — Sequencer + ecosystem.
sequence/Action engine + guaranteed Midfield park;.vexbotpaths[]import +FollowTrajectory;SHUL/2handshake + replay; min-velocity chaining; on-robot tuner. Milestone: VexBuilder replays a real run; a dense goal-to-goal skills run flows without stop-and-go and always ends parked.
10. The Onboard-Estimation Story & Award Narrative¶
The onboard "AI" = a student-built state estimator (complementary filter → EKF) fusing wheel odometry + IMU + GPS + AprilTag, plus onboard CV on the Pi / AI-Vision and offline system-ID / auto-tuning that emits constants, not code.
Award narrative (Excellence / Design / Innovate): "We built our own multi-sensor state estimator
for absolute, drift-free holonomic localization — beyond LemLib's tracking-wheel-only
dead-reckoning." Innovate headline: fused absolute correction + holonomic visual-servo docking that
strafes to align (impossible on a differential drive), tied to <RSC4> (GPS strip) and the goal
AprilTags, mapped to the Skills math (yellow Pins via a sensor-confirmed SetQuadrantToggle; the
Midfield park).
Tiering note: complementary-filter-before-EKF is partly a simplicity choice — a simpler filter is easier to get right and to explain.
11. Red-Team Gates & v1 Cut-List¶
Gates (must do):
1. Freeze ONE coordinate frame, unit-tested, before any code (§7).
2. AI Vision AprilTag SDK — RESOLVED (2026-06). Native AprilTag detection ships in PROS 4.2.2
(pros::AIVision: 4 tag families tag_21H7/16H5/25H9/61H11, enable_detection_types(tags),
set_tag_family, returns tag ID + corners). This repo was pinned to the stale kernel 4.1.0
(calypso is on 4.2.2) — fix is a one-time pros kernel bump. The team already has a working
pros::AIVision wrapper (calypso/Downloads/ai_vision.hpp, object-mode); it becomes the
hal/pros IVision/ITagSource adapter, extended to read tag detections. The V5 AI Vision Sensor
is now the easy primary vision backend; the Pi drops to optional/redundant (stretch). Action: bump
shulib's kernel 4.1.0 → 4.2.2 and vendor/extend the existing wrapper in Phase 1.
3. Tier the estimator — ship the complementary filter first; EKF is Phase 3, gated on the
complementary tier demonstrably bounding drift with GPS-only correction.
4. Add voltage/brownout compensation (FF is voltage-based; battery sags <11V by 60s), a
motion watchdog (old while(fabs(error)>1) loops can hang forever), and a numeric accuracy
target (§7).
Cut from v1 (defer): 5-state EKF + ZUPT + latency comp + adaptive-R (→ Phase 3); AprilTag
PnP / visual-servo docking (→ gated on backend; Distance-sensor docking fallback for v1); S-curve
profiles (trapezoid only); offline auto-tuning / learned-friction tables / on-robot live tuner
(hand-tune kS/kV/kA via one ramp routine); min-velocity motion chaining (stop-and-settle first);
full Action-combinator engine (hand-written blocking calls + one async handle + a wall-clock park
guard); Distance/Optical as fusion inputs (use for manipulation confirmation only); binary
compiled-path artifact + full schema-hash handshake (versioned JSON .vexbot first).
12. Accuracy Risks to Watch¶
- Frame-convention mismatch — #1 killer; resolved by §7 + a regression unit test.
- IMU drift over 60s — cold-calibrate at boot, never move during calib; characterize per-boot bias; GPS-yaw as a slow heading anchor at low yaw-rate; gate GPS out at high yaw-rate.
- X-drive wheel slip corrupting predict — dedicated unpowered perpendicular tracking wheels are a hard requirement; don't value-engineer to drive-motor encoders. Scale Q with travel.
- GPS availability / latency / occlusion near the tall center goal — characterize the real field envelope; default GPS trust LOW; dead-reckon-only mode for Driving Skills.
- Corrector pose-jump mid-motion — innovation-bounded, covariance-weighted, gated nudge (never snap); clamp per-tick nudge; log every gating decision.
- Calibration burden (wheel scale/offset, GPS lever-arm, camera mount, IMU bias) — ship documented routines + persistence (SD/config); make it a Phase-3 entry gate.
- Terminal nesting needs <0.25″ → closed-loop docking (Phase 3) + mechanical funnel; don't promise sub-inch nesting on pose estimate alone.
13. Open Decisions / TBD¶
| # | Decision | Recommendation | Status |
|---|---|---|---|
| 1 | Coordinate frame (+X/CCW vs +Y/CW) | +X / CCW (§7) | LOCKED 2026-06-08 |
| 2 | Accuracy targets (§7) | heading < 1.0° (hard); ~1.0″ pos; ~0.25″ docked |
LOCKED 2026-06-08 |
| 3 | Units system | Minimal header-only Quantity<Dim> over 6 dims (length, angle, time, velocity, acceleration, voltage; canonical = inch, radian, second, in/s, in/s², volt). Angle/Rotation2d is the wrapping heading type; _deg constructs an auto-wrapped Angle, not a bare Quantity. Literals are convenience-only (roadmap M0 = source list). |
Locked 2026-06-19 |
| 4 | GPS/AprilTag aggressiveness | Innovation-bounded, covariance-weighted, per-tick-clamped gated nudge (never snap) + hard dead-reckon-only flag. relocalize() feeds this corrector — it does not snap. Clamp/gate/gain are M3-tuned (mechanism locked, numbers open); complementary tier realizes the same contract heuristically. |
Locked 2026-06-19 |
| 5 | H-drive model | strafeAuthority() = pure read-only query returning the max sustainable lateral speed as a fraction of maxLinearSpeed (XDrive 1.0, HDrive ~0.35 sysid-measured default, not a constant, Tank 0.0); the motion/Chassis layer reads it to clamp commanded vy + trigger the fallback — kinematics does not clamp inside toWheels(). Telemetry-visible (strafeFallbackActive). |
Locked 2026-06-19 · clarified at C3 (2026-08-06), see note |
C3 clarification to decision #5 — semantics sharpened, decision unchanged. Two wordings above were imprecise and are corrected here rather than silently in place, because #5 is a locked row. 1. The cap is absolute, not a ratio. The original text said max sustainable
|vy|/|vx|. C3 confirmed the physical limit is the strafe wheel's own sustainable surface speed — an absolute lateral cap independent ofvx. The literal ratio reading is wrong in both directions: it forbids legal pure strafe (vx = 0), and it over-admits lateral speed at highvx. The correct form is|body vy| ≤ authority · maxLinearSpeed. This is what C1's clamp already implemented (its decision D11, flagged "awaits C3 confirmation" — confirmed). 2. The fallback is turn-while-drive, never sequenced. "Turn-then-drive" names the outcome (heading migrates so the drive can carry the motion), not a sequenced turn followed by a translation. Sequencing is rejected at engine level: it breaksStrafeTo's held heading, invents mid-field headings nobody asked for, and is measurably slower — an aligned-headingMoveToPosecovers 40 in in 1.92 s versus 2.71 s for a pure capped crab. The decoupled per-axis loops produce the migration naturally; nothing sequences anything.Elsewhere in this document "turn-then-drive decomposition" (§4, §4a) describes LemLib's motion model and is unrelated to this fallback. | 6 | Estimator depth | Complementary first → EKF Phase 3 | Locked (tier-it) | | 7 | Vision backend | Pi and V5 AI Vision behind
ITagSource| Locked (both) | | 8 | Skills routing / robot roles | X = tall scorer, H = Toggle-owner + parker (§14) | Filled; build-team decisions open | | 9 | Settling & filtering primitives | OkapiLib-styleSettledUtil(err+deriv+time) on all motion exits; composablefilters/(EMA/median) for task-sensor/alignment conditioning only — excluded from the EKF measurement path (§4a, §13 #4) | Locked 2026-06-19 | | 10 | Robot-config seam |.vexbot→ codegenrobot_config.hpp(primary) + optional SD-card runtime loader (samerobotProfileschema → sameRobotConfig); both behindIRobotConfig(§16.2). Interface = Freeze F4/M1, schema = Freeze F7/M5. | Locked 2026-06-19 | | 11 | Path command model | Command-id registry (runner.on("intake_in",…)), NOT embedded C++code_template. shulib owns the canonical id manifest (VexBuilder reads it); unknown id → WARN+skip; optional typed-args on markers (§16.3) | Locked 2026-06-19 | | 12 | Canonical ecosystem formats | shulib defines therobotProfile+pathssub-schemas inside.vexbot(+ command-id vocab) andSHUL/2; VexBuilder produces them..shupaths+ standalone planner retired (§16.1) | Locked | | 13 | Sim seam timing | Define theSHUL/2schema +hal/simseam at design time (so M6 is a plug-in); implement the wire + sim adapter at M6 (F9); wire to Rapier at VexBuilder Phase 7. Scope =hal/sim+ wire sink only (does not moveTermSinkM2 /SdSinkM3);hal/simis exercisable viahal/fakeeven if Rapier never lands. | Locked 2026-06-19 | | 14 | VexBuilder must add | Explicit drivetrain fields (kind/trackWidth/wheelDia) in.vexbotso config isn't geometry-inferred (§16.2 caveat) | Cross-team ask | | 15 | Kinematics backend | HYBRID:IKinematicsis the interface (swerve is nonlinear — a coeff table can't express it — and the interface is the home forforward()inverse-kinematics used by odometry + thestrafeAuthority()query). The linear holonomic drives (X/H/tank/mecanum) are one impl,MatrixKinematics, driven by a per-wheel[h, v, turn]coefficient matrix (salvageslodge's data-table idea).toWheels()is clamp-free (§13 #5);desaturate()is a virtual with a uniform-scale default. Wheel outputs are dimensionedVelocity, not bare doubles (F3). Frozen as F5 at M1. | Locked 2026-06-19 | | 16 | Adapter failure idiom (settled at R1a) | screen → hold-last-good → expose. Everyhal/prosadapter screens the SDK's error sentinel before converting, then holds its last good value (never zero — a zeroed encoder reads as "stopped", the lie that hides a runaway) and exposes afaultedReads()counter. The RAISE stays in the loop layer, becausehal/sits belowdiag/and raising is policy. Rejected: adapters taking a fault latch (layering inversion); silent zero. Note this supersedes the earlier prediction that validity-free sensors would gain anisConnected()— they did not;isConnected()exists only onIController. | Locked 2026-08-13 | | 17 | PROS include spelling in adapters | QUOTED (#include "pros/…"), always. Measured at R1a: PROS'scommon.mkresolves includes with-iquoteonly, so the angle-bracket form compiles in the host build and every editor and fails the one build that ships. Pinned by a textual guard test. | Locked 2026-08-13 | | 18 | Digital-input seam, built ahead of its consumer | BuildIDigitalInanyway (done at R1b, register row F14, not frozen). A deliberate departure from F1's standard of earning a seam on a real consumer's verb: the lift-homing question below is unanswered, and discovering the need at hardware-validation time with the robot on the bench is far more expensive than a small unused sibling now. The cost is accepted in writing rather than discovered. | Locked 2026-08-14 |
Still open — these are decisions, not tasks:
| # | Open question | Why it is still open | Who consumes the answer |
|---|---|---|---|
| A | Lift homing: limit switch or motor stall? | Asked 2026-08-13, undecided. The seam exists either way (decision 18); if the answer is "stall", IDigitalIn is a small unused sibling and the stall detector already shipped at F1. |
The scoring-primitives chunk (Phase F′), which writes the homing routine |
| B | Which sensors actually go on the competition robots? | The ranked recommendation is in the project briefing; the build team has not committed. Ports are finite and the ranking is accuracy-per-port. | Hardware validation, and every register entry that depends on a sensor existing |
| C | When to characterize the drivetrain. | Doing it on the only robot that exists produces a validated tool and throwaway numbers — gains never transfer across chassis, so a measured-looking wrong number is worse than an honest placeholder. Deferring it until a competition robot exists trades schedule for reusable results. | The gain-measurement chunk, and the order of the remaining phases |
| D | Does the available robot have an ADI expander? | Reported at the first bench session from an out-of-range registry index, so the reading may be garbage; never re-checked. The brain's 8 built-in ADI ports exist regardless, so nothing is blocked — but the number of available 3-wire ports is unknown. | Anything that budgets 3-wire ports (limit switches, pneumatics) |
14. Skills Strategy & Routing¶
Synthesized from 16 Override strategy/design video transcripts (digest run 2026-06).
The one insight that dominates: ownership, not volume¶
An owned yellow Pin = 10 pts and double-counts across clear zones (~20 effective); an
unowned yellow ≈ 0. So a single setQuadrantToggle (flip a quadrant's Toggle to our color)
outscores several raw cycles — a ~40-pt swing per quadrant. The two highest-value software
primitives are setQuadrantToggle and orientToScoringHalf (yellow-side-out). Spend the
accuracy budget there first. Alliance goals are protected and neutral/opposing goals can't be
de-scored → build the tall stack on a protected alliance goal; rush the Midfield goal early only
for denial value.
Recommended two-robot role split (don't contest the same quadrants)¶
- 24" X-drive = primary tall scorer. Floor pickup (counter-roller + widest poly funnel) + ~50" lift + 180° rotator (yellow-side-out) + AI Vision AprilTag for mid-run relocalization. Owns its home quadrant; builds one tall owned-yellow nested stack on a protected alliance goal.
- 15" H-drive = Toggle-owner + match-load cycler + parker. Self-aligning vertical clamp (standing/match-load only — sidesteps floor pickup, the season's hardest unsolved problem) + poly-cutout passive dock. Owns the other two Toggles and parks the Midfield (+8) at the buzzer.
Two 60s routing concepts (separate per-robot autons)¶
- Concept A — X-drive tall-tower (~90–130 pts): own home Toggle (t0–4) → preload + deny the
Midfield goal early → ~8–10 cycles of
intakeUntilCapture → relocalize/dockToGoal → liftToLevel → orientToScoringHalf → buildStack(t10–48) → re-verify Toggle (t48–56) → hold/brake on it. - Concept B — H-drive Toggle-own + cycle + park (~60–90 pts): flip its two Toggles, wedge-jam
(t0–8) → ~6–8
matchLoadCycle()(t8–45) → sweep loose pins (t45–55) →endInMidfield()+8 (t55–60). - Combined ≈ 150–220+ pts, dominated by owned-yellow stacking — not raw cycle count.
The four accuracy bottlenecks (each maps to a primitive)¶
- Floor-pickup reliability →
intakeUntilCapture(counter-roller, retry, sensor-confirm). - Per-level lift height repeatability under growing load + cascade back-drive →
liftToLevel(homing + linkage-aware mapping + sag-comp PID hold). - Yellow-half-out orientation →
orientToScoringHalf(color-sense → pass-through vs flip). - Odometry drift over 8–10 cycles in a crowded field →
relocalize(AprilTag/poly-cutout fix before each score). This is the §8 localization stack earning its keep.
Skills-layer primitives the library must expose¶
| Layer | Primitives |
|---|---|
| motion | fieldCentricStrafe / strafeTrim (H), moveToPoseProfiled (lift-state-aware accel), holdPose/driveBrake |
| alignment | dockToGoal (poly-cutout/AprilTag; current/distance/pose confirm; height-adaptive fallback), dockToLoader, relocalize(featureSource) |
| manipulation | setQuadrantToggle (index N clicks on the 3-state Toggle + color confirm), intakeUntilCapture, liftToLevel, orientToScoringHalf, rotateClampToAngle (profiled, no fling), clampActuate+clampConfirm, deployActuator (air-budget aware) |
| sequence | buildStack (align→lift→orient→place pin→cup-over-pin→confirm nest), matchLoadCycle, endInMidfield (18" height lockout), strategyMode(tallTower\|fastCycle), time-budgeted Sequencer (possession-aware: 1 pin + 1 cup; aware of the 2 pre-scored pins; guaranteed end-of-run action) |
Non-negotiable: time-budgeted sequencer with a guaranteed end-of-run action¶
No driver recovers a stall. The +8 Midfield park and the final Toggle re-verify must fire on a
hard schedule regardless of where the scoring loop stalled — a run-scoped deadline owner is
required, not optional. (An earlier draft of this sentence said "extending the existing
RobotCommands/Command queue"; the C6 audit found that queue never had an executor and C7
deleted it. The requirement is DELIVERED as sequence/run_guard.hpp — chunk F2, 2026-08-13:
caller-supplied instants and end action, no field knowledge in the library; the park pose and the
Toggle re-verify themselves are F4/Phase F′ strategy the students author.)
Build-team decisions to settle (mostly hardware, not software)¶
- Robot roles — X = tall floor scorer / H = Toggle-owner + parker (recommended). Drives the auton split.
- Mechanisms — X: flexwheel + counter-roller + lift + 180° rotator; H: self-aligning vertical clamp.
- Lift — cascade (lowest CG, needs homing) vs DR6B (simpler, tippier). Recommend cascade on X.
- Goal localization — AI Vision AprilTag on the X-drive scorer; poly-cutout dock on the H-drive.
- Toggle defense — wedge-jam where reachable + a ~t50s re-verify pass (low priority in skills — no opponent).
VEX U note: no 55W drive cap — we can run heavier X/H drives and dual-motor lifts; treat the HS/MS 55W RPM figures (2.75" @ ~450 rpm) as analog targets, not caps.
15. The One-Stop-Shop Capability Catalog (past · present · future)¶
The north star: shulib is not "a chassis class" — it is the complete autonomy stack for a VEX U holonomic robot, from the build file to the scored point, usable by a team that cannot write C++ and extensible by one that can. This catalog is the whole surface area. We tier it (§3) so the proven subset ships first; nothing here is hand-waved away, but nothing ships unproven either.
Maturity legend: ● Core (proven v1 subset, ships first) · ◐ Tiered (Phase 2–3, clearly reachable) · ○ Frontier (stretch / research — the "future" we design seams for now).
| Domain | ● Core (present) | ◐ Tiered (near future) | ○ Frontier (north star) |
|---|---|---|---|
| Drivetrain & kinematics | IKinematics twist (vx,vy,ω); XDrive/HDrive/Tank |
Mecanum, swerve scaffolds; per-wheel slip model | Auto-ID drivetrain from VexBuilder geometry; online wheel-radius calibration |
| Motion primitives | MoveToPose, TurnTo, StrafeTo, driveBrake, holdPose |
FollowTrajectory (per-waypoint heading), min-vel handoff chaining |
Dynamic replan around detected obstacles; time-optimal profiling |
| Path & trajectory | .vexbot paths[] import + profiled execution; markers/commands |
In-lib spline/Squiggles-style smoothing; velocity-constrained profiles |
Live VexBuilder round-trip (edit path → re-sim in seconds) |
| Localization | Pilons odom + IMU-owned heading; IPoseSource |
GpsCorrector + AprilTagCorrector; complementary filter; Mahalanobis gating |
Error-state EKF on SE(2); LIDAR scan-match; Pi-fused absolute pose |
| Control & FF | derivative-on-measurement PID + integral clamp; SettledUtil exits; kS/kV/kA FF |
brownout/voltage comp; motion watchdog; per-axis decoupled servo | adaptive gains; learned feedforward from logged runs |
| Manipulation | Mechanism HAL, intakeUntilCapture, liftToLevel, profiled rotators |
buildStack, matchLoadCycle, air-budget-aware actuation |
vision-servo grasp; closed-loop stack verification |
| Autonomy authoring | data-driven PathRunner + command registry (no C++ needed) |
fluent recipe API; time-budgeted Sequencer w/ guaranteed end action | GUI sequence builder in VexBuilder writing paths[] into .vexbot |
| Simulation & test | host doctest suite via hal/fake; deterministic clock |
hal/sim adapter + SHUL/2 over VexBuilder agent socket |
full Rapier physics round-trip; planned-vs-actual overlay |
| Diagnostics & telemetry | TermSink readable terminal stream + DebugRecord + fault codes + per-motion result (§18); NullSink default |
SdSink binary blackbox; SHUL/2 wire; live PID/FF tuner; record/replay |
on-brain HUD add-on; cloud run library; auto-tune from replays |
| Config & hardware | IRobotConfig + RobotBuilder; hand-written config |
.vexbot → robot_config.hpp codegen; SD-card runtime profile |
zero-touch: build in VexBuilder → working robot, no edits |
| Docs & onboarding | per-module notes | "first auton in 10 min" path; recipe cookbook | generated API site + interactive examples (the team website) |
The three columns ARE the roadmap's spine (§9, and the public roadmap.md). Anything a future
team asks for should map to a cell here; if it doesn't, the catalog grows — deliberately, tiered.
16. VexBuilder Integration Contract¶
VexBuilder is the team's single authoring tool — 3D robot designer + path planner + (planned)
physics sim (Tauri / React / Rust+SQLite) — saving everything in one .vexbot project file.
shulib is the runtime: it ingests that file and runs it, today and for robots not yet designed.
Decision (locked): the standalone .shupaths format and the separate Python path planner are
retired — paths now live inside .vexbot alongside the robot, so a routine can never fall out
of version-sync with the robot it was drawn for. A one-way importer migrates existing .shupaths.
16.0 — Principle: shulib defines the contracts; VexBuilder implements them. Because shulib's
core depends only on the PROS-free HAL (§5), the exact same motion/localization/auton code
runs in three targets with zero changes: real robot (hal/pros), VexBuilder sim (hal/sim),
host tests (hal/fake). "Works with the simulation" is therefore a structural guarantee, not a
feature we bolt on. At the tool boundary shulib reads just two contracts — the robotProfile +
paths sub-schemas inside .vexbot, and the SHUL/2 telemetry wire. VexBuilder owns the .vexbot
file; shulib co-owns the slices it reads (plus the command-id vocabulary) and ignores the rest
(parts/holes/joints).
16.1 — The format divergence we must resolve (finding). VexBuilder's spec docs and its shipped implementation have already drifted apart. shulib picks the canonical shape and collapses the path tooling into the one file:
| Artifact | Spec docs want | Shipped .vexbot v2.0.0 has |
shulib canonical decision |
|---|---|---|---|
| Robot config | mechanisms[] + shulibExportConfig + codegen robot_config.hpp |
.vexbot electrical{motors,sensors,pneumatics} + joints[] (electrical UI not built — arrays empty) |
robotProfile block shulib specifies, written into .vexbot; codegen to robot_config.hpp primary. Source data from the shipped electrical{} + explicit drivetrain fields VexBuilder must add. |
| Paths | separate .vbpath |
separate .shupaths (Python planner: x,y,heading,heading_mode,motion_type,reverse,commands_after[{id,code_template}]) |
Folded into .vexbot as project.paths[] — one file, version-locked to the robot. .shupaths + standalone planner retired; a one-way importer migrates old .shupaths. Drop embedded code_template for command ids (§16.3). |
| Sim/telemetry | Rapier 60 Hz + IPC events (Phase 7) | Tauri agent server only (server.json: port,token,pid) |
SHUL/2 wire protocol over that local socket; defined now, wired when Rapier lands. |
16.2 — Seam 1: one project file in (.vexbot → config + paths). shulib reads a single .vexbot
and produces an in-memory RobotConfig and the robot's Routes. The config slice it specifies:
.vexbot › project.robotProfile {
identity{name,team,season};
drivetrain{ kind: x|h|tank; wheelDiameter; trackWidth|geometry; motors:[{port,reversed,cartridge}] };
odometry{ wheels:[{port,reversed,diameter,offset}]; imuPort; gpsPort };
sensors:[{port,type}]; mechanisms:[{name, motors:[...], pneumatics:[port]}]; corrections{x,y,theta} }
.vexbot › project.paths[] (the routines — see §16.3)
Two ingestion paths, same in-memory types behind IRobotConfig / IRouteSource:
- Codegen (primary, on-robot). A host-side generator (a small shulib CLI, or a VexBuilder
export button) reads
.vexbot→ emits a typedrobot_config.hppcarrying both the profile and the routines (inline constexpr). No runtime JSON parse, no SD dependency, compile-checked. - SD-card runtime (optional). shulib can also read the
.vexbotdirectly off the SD card at boot — so a non-coder re-exports a file and re-runs without recompiling. Same interface, gated.
RobotBuilder.from(profile) returns a fully wired Chassis (drivetrain + kinematics + localizer +
sensors); runner.load(route) arms a routine. "VexBuilder file → working robot + auton" is two
lines. Standalone preserved: a code-fluent team can build RobotConfig and Route directly in
C++ with no file at all — .vexbot is the on-ramp, not a dependency. Honest caveat: the shipped
.vexbot doesn't yet carry drivetrain type / track width / wheel diameter as first-class fields —
today they'd be inferred from part geometry (brittle). Action: VexBuilder must emit them
explicitly (Open-Decision #14); shulib ships an inferDrivetrain() fallback but treats explicit
fields as the contract.
16.3 — Seam 2: path execution + command registry. project.paths[] holds routines (waypoints:
x,y,heading,headingMode,motion,reverse,constraints,markers); shulib's PathRunner executes
profiled motion per segment with marker callbacks. The legacy planner embedded C++ snippets
(code_template:"mech.intakeIn();") — a data/code coupling we reject. Instead markers carry command
ids ("intake_in") and the student registers handlers once. The intended spelling,
sketched — PathRunner does not exist yet, so this is design, not code you can write today (and
it is fenced as text for that reason: every cpp block in this project's documentation is a
compiled, verbatim-quoted example, and a sketch of a future API cannot be one):
This is the keystone of the no-code story (§17): a routine is data a non-coder authors in
VexBuilder, executed by a library the coders maintain. (A codegen mode may emit the same
id→handler bindings as a generated runner.on(...) registration table at compile time — never
free-form code, so the data/code split holds.) shulib owns the canonical command-id vocabulary,
exported as a manifest VexBuilder reads to populate its command picker; an unknown id logs a WARN
fault and is skipped, never crashes (§16.5, §18.4). Markers may carry an optional typed-args object
(e.g. {id:"lift_to_level", args:{level:3}}) so parametric primitives don't explode into one id per
value. The .shupaths importer maps old code_template strings to ids best-effort, flagging any it can't.
16.4 — Seam 3: Simulation & telemetry (SHUL/2). When Rapier lands, shulib runs unmodified
in-sim through hal/sim, which speaks SHUL/2 over VexBuilder's agent socket (discovered via
~/.local/share/com.gonzei.vexbuilder/agent/server.json). Bidirectional: VexBuilder feeds simulated
sensor readings in (so the same estimator runs), shulib streams pose/twist/wheel-cmd/markers out
(so VexBuilder renders the ghost robot and overlays planned-vs-actual). Versioned and defined now
so Phase 7 is a plug-in, not a redesign.
16.5 — Versioning & forward-compat (future build files). Every contract carries schemaVersion;
shulib negotiates and migrates additively (mirrors VexBuilder's own migrateProject() 1.1→2.0).
Unknown newer fields are ignored, not fatal; missing fields get safe defaults. Bonus of one file:
a robot and its routines share a version and can't drift apart — the spec's unsolved "re-export when
the robot changes" sync problem disappears. The promise holds: a .vexbot you make next year still
drops into the library.
17. Accessibility & Progressive Disclosure (for teams that can't code yet)¶
The pillar: a future team that cannot write C++ must still field a working, accurate, award- caliber autonomous — and a team that can code must never hit a ceiling. shulib delivers this with progressive disclosure: four tiers, each a strict superset of the one below, no cliff between them.
| Tier | Who | What they touch | What they get |
|---|---|---|---|
| 0 — Zero-code hardware | Anyone | Build the robot in VexBuilder | robot_config.hpp — no hand port-mapping, no measuring (§16.2) |
| 1 — Data-driven auton | Non-coder | Drag waypoints + pick commands in VexBuilder | saved into .vexbot → PathRunner runs it. A full routine with zero C++. |
| 2 — Recipe API | Beginner coder | ~10 readable lines | r.moveTo(p).then([&]{ intake.in(); })… — fluent, hard to misuse. (Corrected at F1: this cell long read chassis.moveTo(p).then(intake.in), which was never valid C++ twice over — verbs return ExitReason, which has no .then() (chains belong to Routine, ruled at D1), and intake.in names a member function without calling it. The compiled, gate-checked form is the guide ch. 9 listing.) |
| 3 — Full API | Programmer | Everything in this doc | kinematics, estimator, custom motions, the works |
The bridge is the command registry (§16.3). A non-coder designs strategy as data; one coder maps command-ids → mechanism actions once. After that, the non-coder iterates autons all season without touching code, and re-running a new path is loading a different file — not a rebuild.
You stay in command. The library and its tools carry out the strategy you design — a routine is your decisions expressed as data; the tools never make those decisions for you.
Onboarding as a first-class feature. "Your first auton in 10 minutes" (build → export → drag a
path → run), a recipe cookbook, and generated API docs that publish to the team website (the
roadmap.md and capability catalog are written to be web-portable). Documentation is a deliverable
with milestones in the roadmap, not an afterthought.
18. Diagnostics & Observability¶
Observability is a first-class, cross-cutting discipline — not a side effect of the telemetry wire. The audit (2026-06) found the old plan scattered observability across other features and parked it in the M6 telemetry bundle, while the legacy code reached for debugging power the wrong way (a ~250-line hot-loop wheel-health block, prose
[VERT]/[VEER]diagnosis trees, ALL-CAPS banners, rawstd::cout). This section makes it a single owned contract. Primary debug surface: the terminal (readable, human-friendly). On-brain HUD is an optional add-on; the SD blackbox is the no-laptop field counterpart.
18.1 — One record, many sinks. A single structured per-tick DebugRecord is the source of
truth; every sink just formats it, so bench / terminal / field / sim traces are directly
comparable. Sinks sit behind the ITelemetrySink HAL seam (M1), NullSink default = zero cost:
| Sink | Role | When | Tier |
|---|---|---|---|
NullSink |
nothing (zero-cost default) | competition build | core, M1 |
TermSink |
human-readable terminal stream — the primary dev/debug surface | every dev/tune session | core, M2 |
SdSink |
compact binary blackbox to /usd/ — the no-laptop field record |
field runs | core, M3 |
Shul2Sink |
the SHUL/2 wire (VexBuilder visualize/record/replay) |
ecosystem | tiered, M6 |
BrainHud |
optional on-brain HUD/summary screen | add-on | add-on / Frontier |
SHUL/2 v1 is the serialized DebugRecord (Freeze F9) — define the schema once, every sink
serializes it (text / binary / wire). TermSink is not the JSON wire; it's a pretty-printer for
humans.
18.2 — The DebugRecord (per-tick snapshot). Captured each control tick, rate-budgeted:
t, dt, target/measured Pose2d, per-axis error, commanded (vx,vy,ω), per-wheel V+I,
IMU yaw+yaw-rate, active command id/state, dead-reckon flag, quality flag, covariance
trace / filter trust weights, gating (residual, Mahalanobis, reason), applied-correction
(dx,dy,dθ) + clampedThisTick (audits never-snap, §13 #4), strafeFallbackActive (§13 #5),
fault code, battery V/I. Competition build routes it to NullSink (≈free); dev build to
TermSink (+SdSink).
18.3 — Readable terminal output (the headline requirement). Leveled
(ERROR/WARN/INFO/DEBUG/TRACE, compile-time TRACE strip off the hot path), tagged by
subsystem, column-aligned (never prose), throttled on high-rate channels, with a one-screen
run-summary at auton end. Target shape:
[t=12.34] [MOT] MoveToPose#7 tgt(24.0,36.0,90°) err(0.4",0.2",0.3°) v(18,4,0.1) ▸run
[t=12.41] [EKF] gps fix ACCEPT resid(0.8",0.5",0.2°) mahal 1.9 Q=0.91
[t=12.50] [MOT] MoveToPose#7 ✓SETTLED final(24.1,36.0,90.1°) over 0.2" drift 0.1° 1.16s
[t=12.51] [WARN][SEQ] intakeUntilCapture retry 1/3 (optical=none)
── RUN SUMMARY ───────────────────────────────────────────
scored 6 pin · 1 cup failed 1 grab (FLOOR_PICKUP_MISS)
heading: max 0.7° final 0.3° gating rejects 4 brownout no
worst loop dt 11.2ms first fault none
build a1b2c3d · routine "redLeftTall" · batt 12.4→11.6V
──────────────────────────────────────────────────────────
The per-motion result line (target vs final · overshoot · drift · time · exit-reason) and the
run summary are the two genuinely-good ideas salvaged from the legacy "logging extreme" code,
re-expressed as structured fields, not essays emitted from motion loops.
18.4 — Fault discipline. A stable numeric fault-code enum (ODO_STUCK, IMU_LOST,
GPS_GATE_REJECT, BROWNOUT, LOOP_OVERRUN, NAN_POSE, MOTION_TIMEOUT, …) + latched
first-fault capture (root cause vs cascade); NaN/Inf + invariant asserts (heading wrapped,
per-tick pose-delta within physical max) that log-and-recover (non-fatal, safe fallback) instead
of propagating a silent NaN; motion exit-reason codes (SETTLED/TIMEOUT/CANCELLED/
FAULT_ABORT/SUPERSEDED) on every IMotion; loop-overrun / tick-timing detection (a blown
10 ms tick corrupts PID dt → degrades the very < 1° heading we promise).
18.5 — Session header / provenance. First record of every run: git build hash + routine id + alliance/side + port map + battery start — lets us compare/reproduce runs and confirm exactly which binary produced a given log.
18.6 — Migration from the current code. KEEP the appetite and the two good ideas
(sensor-health/stuck detection → rebuilt generic, gated, off the hot path, feeding fusion
sensor-validity; the per-motion result record → a structured field). DROP the bad
implementation (the hot-loop wheel-health block with hardcoded ports 8/9/10, the prose hardware-
diagnosis trees, the !!!!! banners, raw std::cout). FIX the three inherited logger.hpp
bugs before anything layers on it: escapeJSONString declared but never applied (frame corruption),
a dead sendDebugMessages decl, and a manual update() racing the background flush task.
18.7 — Tiering (pulled forward — the key fix). Observability does not wait for M6.
TermSink + DebugRecord + fault codes + per-motion result + loop-overrun + the logger bug-fixes
land at M2, so the M2–M3 localization/control milestones — where we fight for < 1° and tune
fusion gates — are field-debuggable as they're built. SdSink blackbox + estimator-introspection
quantities (residual/Mahalanobis/covariance) land at M3 (the accuracy milestone's evidence).
SHUL/2 wire + replay stay M6; the on-brain HUD is an add-on; cloud/auto-tune stay Frontier.
19. Glossary & References¶
- Override scoring objects: Pin (two colored halves, ~1.6″×6.5″), Cup (transparent + opaque, ~3.15″×6.5″), Goal (9; tall center "Midfield" + 4 short neutral + 4 alliance), Toggle (4, one/quadrant, 3 states), Loader (match-load intake).
- Pilons odometry: the 5225A arc-based 3-wheel tracking method (kept as a pure function).
- Key rules:
<VUR1>(two robots 24"/15"),<VUR12>(onboard processors legal),<VUG3>(LIDAR/spinning sensors),<RSC4>/<VURS1>(skills field setup + GPS strip),<R13c>(vision wireless off in match).
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.