# The Elite & League Architectures https://frc-code-scout.jointheleague.org/ # Inside Competition Robot Code — The Elite and League Architectures **A four-part wiki.** Part I documents the **Elite Architecture**: the architecture that top FRC teams have actually converged on, reconstructed empirically from reading dozens of public codebases and checked against competition results (the correlation is moderate and heavily confounded by program maturity — see ch. 34). Part II is the **anatomy** — a component-by-component reference that opens the hood on each major piece. Part III presents the **League Architecture**: our own evolved proposal, the same ideas pushed to a single unifying abstraction and made language-portable. Part IV is the **measurement instrument** — the rubric, its calibration, and a four-year case study. > **This page is the annotated table of contents** — the hierarchy, a description of each chapter, and > the source documents in `knowledge/` (and `docs/review/`) each chapter is written from. Some cited > sources have since moved under `knowledge/archived/`; the crosswalk records each file's disposition. This wiki is > **self-contained and supersedes** the generated `docs/book`; chapters absorb the cited source material > rather than linking out to it. The full source-to-chapter map is > [Appendix C — the crosswalk](appendices/source-crosswalk.md). --- ## How to read it The wiki is hyperlinked like a wiki, but it has a spine you can read straight through like a book. **Part I builds the case and explains what the architecture is** — at a glance, not in great detail. **Part II is the engineering reference** — how each component is actually built. **Part III makes the argument** that all of it is special cases of one shape, and that naming the shape buys logging, replay, testing, and ROS portability at every scale. The dividing line between Parts I and II is **depth, not topic**. Part I answers *what is it, is it true, and does it matter*; Part II answers *how does it work and what do I build*. A reader who finishes Part I knows what the three seams are and how a goal becomes a motor voltage — in a sentence or two each. Part II is where the hood comes off. - **New to the project?** Read Part I straight through (ch. 1 → 9). - **Building a robot?** Read the five-views overview (ch. 2), then the rubric and maturity ladder in the [How We Developed This](appendices/how-we-developed-this/) appendix, then work through Part II for the mechanics, then Part III. - **Need the detail on one component?** Jump straight to its Part II chapter (ch. 15 → 23). - **Evaluating the proposal?** Part III (ch. 24 → 32), ending on the open questions in ch. 32. - **Scoring a team?** Read the rubric in full (ch. 33), then the caveats in the San Diego scoresheet (ch. 34), and use the Patribots study (ch. 35) as the worked example. The numbering is the book order. The lettered sections are the shelf it sits on. One note on the numbers: chapters 10–14 (and section letter E) don't exist — that material became [Appendix A](appendices/how-we-developed-this/), and the numbering is preserved so older citations still resolve. Nothing is missing. --- # Part I — The Elite Architecture *(descriptive: what top teams actually do)* *The architecture nobody designed and everybody arrived at. Reconstructed from the corpus, turned into a measurement instrument, and checked against competition results.* ## A. Orientation ### 1. The baseline and the shape The shared starting point and the two lenses the rest of Part I reads the architecture through. WPILib command-based — subsystems (things the robot *has*) and commands (things it *does*), wired in `RobotContainer` — and the two joints of coupling it leaves (command → concrete subsystem, subsystem → concrete devices) that every elite addition targets. Then the framing for the whole part: **positive space** (the views — where the parts are) and **negative space** (the seams — the joints between them), carrying the organizing rule **build the seams, defer the payoffs.** *Sources: `corpus-analysis/02-frc-37-team-survey.md` (baseline + "modularity is a ladder"), `build-spec/elite-architecture.md` (§1).* ### 2. The architecture in five views The orienting overview, built on Kruchten's **4+1 view model**. The *logical* view (the six recurring components and how command flows down while state flows up), the *development* view (the library layering — WPILib floor, vendor libraries, and the rule that confines them below the IO line), the *process* view (the 20 ms read → log → decide → actuate loop and the path of a driver packet), the *physical* view (the robot schematic — roboRIO, the CAN and CANivore buses, coprocessors on Ethernet), and two *scenarios* that put the views in motion. A reader who stops here understands the architecture; the seams and Part II add resolution. *Sources: `build-spec/elite-architecture.md` (§1–§2 thesis + layered overview + runtime loop).* ## B. The seams *(the negative space — the joints between the parts)* ### 3. The IO seam — the spine The most important pattern in the corpus, stated and motivated: one `XxxIO` interface per subsystem at the line between subsystem logic and physical devices, with interchangeable real/sim/replay implementations — the **Strategy pattern at subsystem granularity**. What it is, how common it is (24/55), and why it is the precondition for sim, tests, and replay. Rubric D1. *(Mechanics — the loop-above/below decision, the inputs struct, naming — are Part II ch. 16–17.)* *Sources: `corpus-analysis/03-io-layer-strategy-pattern.md`, `build-spec/subsystems/00-anatomy-of-a-subsystem.md`.* ### 4. The state seam — `RobotState` and the world model A single object owning the robot's best estimate of the world behind a pose estimator: sensors write, decisions read. The corpus split that matters — pose estimation is near-universal (`addVisionMeasurement` in 50/55), but a *centralized* world model (26/55) is the elite move. Rubric D7. *(The estimator internals and fusion mechanics are Part II ch. 20.)* *Sources: `build-spec/subsystems/07-robotstate.md`, prevalence table in `rubric/rubric.md`.* ### 5. The coordination seam — the superstructure Where teams actually diverge. One robot-wide *goal* fanned out to per-subsystem setpoints through a single guarded transition function — intent separated from execution, with interlocks in one place. Names the six coordination paradigms the corpus exhibits without yet dissecting them. Rubric D2. *(The paradigms in depth — FSMs, state graphs, behavior trees — are Part II ch. 22–23.)* *Sources: `build-spec/subsystems/08-superstructure.md`, `corpus-analysis/02-frc-37-team-survey.md` (coordination paradigms).* ### 6. The drivetrain — the architecturally special subsystem Why the drivetrain gets its own chapter even in the overview: it is the only near-universal subsystem (94%) and the only thing that is both actuator and primary sensor — its `Pose` is the most-consumed value on the robot. The architecture spectrum from CTRE-generated (~63%) to teams that own a real seam (~27%). *(The swerve internals — modules, kinematics, odometry — are Part II ch. 19.)* *Sources: `corpus-analysis/08-drivetrain-as-architecture.md`.* ## C. The practices around the seams ### 7. Cross-cutting practices — simulation, testing, logging The three disciplines that hang off the IO seam and separate engineering culture from cargo cult. Simulation (run modes, HAL sim, maple-sim, AdvantageScope — D3); testing (IO-sim-as-mock, the sim-time harness, CI — D4); logging (the inputs-struct contract; the println → DogLog/Epilogue → AdvantageKit-with-replay ladder — D5). The corpus truth: almost everyone builds the seam, almost no one collects the test/replay dividend. *Sources: `build-spec/simulation.md`, `build-spec/testing.md`, `build-spec/logging.md`.* ## D. The frontier ### 8. Alternatives — legitimate deviations Sound, uncommon, situational patterns that aren't the default but earn a place: capability-typed devices (named by capability, not vendor), physical-plant simulation (truth as the dual of estimate), state-graph coordination, and behavior trees — each with its guardrails and an honest statement of when it's right and when it's over-engineering. The bridge from "what teams do" to "what we'd do differently." *Sources: `alternatives/README.md`, `alternatives/01-capability-typed-devices.md`, `alternatives/02-physical-plant-simulation.md`, `alternatives/03-state-graph-coordination.md`, `alternatives/04-behavior-trees.md`.* ### 9. Other advanced topics Additive techniques that specialize a seam rather than replace it — the category next to the alternatives: state-space/LQR, the swerve setpoint generator, high-frequency threaded odometry, self-check diagnostics, replay-as-regression-test, neural game-piece detection, reactive autonomy, and QuestNav. Each attaches at an existing seam, keeps its vendor dependency below the IO line, and can be added or removed without disturbing the rest of the robot. *Source: `build-spec/other-topics.md`.* --- # Part II — Anatomy of the Elite Architecture *(reference: how each component is built)* *The hood comes off. Each chapter takes one major component of Part I and shows the engineering — the contracts, the decisions, the real variants in the corpus, and what to build.* ## F. The control path and abstraction ### 15. The control path, end to end The orienting map for the whole reference: how a teleop binding or an autonomous routine becomes a superstructure goal, becomes a subsystem setpoint, becomes an IO call, becomes a motor voltage — and how measured state flows back up the same path. The 20 ms loop in order (read → log → decide → actuate), run-mode selection (REAL / SIM / REPLAY), and the single point that chooses implementations. Every later chapter is a drill-down into one station on this path. *Sources: `build-spec/elite-architecture.md` (§2.3 runtime loop, §5 scenarios).* ### 16. Hardware abstraction and the IO line What "hardware abstraction" means precisely versus "the IO layer as a *location*" — they are not the same thing, and conflating them is a common error. Ports-and-adapters at subsystem granularity; the two deliberate decisions (where the control loop sits — loop-above vs loop-below the line; inputs struct vs plain getters); and the vendor-confinement rule, including how it actually leaks (22 of 24 IO teams import a vendor type above the line). *Sources: `corpus-analysis/03-io-layer-strategy-pattern.md`, `build-spec/subsystems/00-anatomy-of-a-subsystem.md`, `build-spec/code-review-principles.md`.* ### 17. Motor interfaces The device-level contract in depth: how teams actually talk to motors. The six reusable `MotorIO` contracts found across the corpus and the design axes that distinguish them; loop placement and on-motor vs on-RIO control; and the capability-typed alternative (named by capability, not vendor) with a hardware object below the line. *Sources: `corpus-analysis/05-motor-io-interfaces.md`, `alternatives/01-capability-typed-devices.md`.* ## G. The subsystems ### 18. Subsystem archetypes The IO quartet (`XxxIO` / `Inputs` / hardware impl / `IOSim`) applied per control archetype, with the WPILib sim model each uses: linear position (elevator, climber — `ElevatorSim`), rotational position (arm, pivot, wrist, turret — `SingleJointedArmSim`), velocity (shooter, flywheel — `FlywheelSim`), and roller / game-piece (intake, indexer — `DCMotorSim` + the game-piece sensor). The shared template and the mock/library/vendor ethic every subsystem applies. *Sources: `build-spec/subsystems/00-anatomy-of-a-subsystem.md` through `04-roller-gamepiece.md`.* ### 19. The drivetrain subsystem The swerve special case in engineering depth: the multi-interface seam (`ModuleIO` ×4 + `GyroIO`), kinematics and odometry, the optional `SwerveSetpointGenerator`, and the empirical architecture spectrum (CTRE-generated vs owned seam) with the 254/2910 elite package layout and the `CommandSwerveDrivetrain` / `SwerveRequest` / `SwerveDriveState` usage rankings. *Sources: `build-spec/subsystems/06-swerve-drivetrain.md`, `corpus-analysis/08-drivetrain-as-architecture.md`.* ## H. Perception and coordination ### 20. The world model `RobotState` and sensor fusion in depth: the pose estimator, the time-interpolated history buffer, how odometry and vision corrections fuse, and why centralizing the estimate (rather than letting the drive subsystem privately own it) is what lets vision, pathfinding, and autos share one consistent world model. The state seam's internals. *Sources: `build-spec/subsystems/07-robotstate.md`.* ### 21. Vision systems The dedicated vision chapter. The pipeline end to end — coprocessor (PhotonVision / Limelight) → observation (pose + tags + latency) → `VisionIO` → `RobotState.addVisionMeasurement` — and the `VisionIO` swap that makes the camera vendor interchangeable. What teams actually run and how far it goes: AprilTag pose estimation (the near-universal floor), neural game-piece detection (the rung past), and emerging VIO / QuestNav. What the system *produces* — pose estimate, target bearing, standard deviations, latency — and exactly who consumes each output (the fusion, the aim, the auto). Std-dev / ambiguity gating: how a measurement is trusted or rejected. *Sources: `build-spec/subsystems/05-vision-sensor.md`, prevalence table in `rubric/rubric.md`, `build-spec/other-topics.md` (neural detection, QuestNav), `corpus-analysis/06-lessons-from-broader-robotics.md`.* ### 22. Coordination I — state machines and the superstructure The coordination seam in depth: the wanted/current FSM, the centralized `RobotManager`, guarded transitions, and where interlocks live (the one object that sees all mechanisms at once). The foundation `switch`-over-goal-enum form and how a real interlock is sequenced safely. The most common real coordinators in the corpus. *Sources: `build-spec/subsystems/08-superstructure.md`, `corpus-analysis/02-frc-37-team-survey.md` (coordination paradigms).* ### 23. Coordination II — state graphs and behavior trees The far end of the coordination ladder: coordination as **graph search** over a superstructure state graph (explicit state graphs in ~5 teams; genuine A\* over a discretized configuration space in 254 alone), and **behavior trees** — the re-ticked SUCCESS/FAILURE/RUNNING tree borrowed from game AI (3015's full runtime + visual editor). When each beats an FSM, and why explicit behavior trees are nearly absent in FRC while their command-group cousin is universal. *Sources: `alternatives/03-state-graph-coordination.md`, `alternatives/04-behavior-trees.md`, `corpus-analysis/02-frc-37-team-survey.md`.* --- # Part III — The League Architecture *(prescriptive: what we propose)* *The Elite Architecture, evolved. Parts I and II's components turn out to be instances of one shape; naming that shape is the whole proposal.* ## I. The unifying idea ### 24. From Elite to League — what we keep, what we change The bridge chapter. We keep every Elite commitment (the IO seam, intent/execution split, vendor confinement, the deferred-dividend discipline) and change one thing: instead of three differently shaped seams plus a pile of subsystems, we observe they are all the **same recursive component** and build to that contract deliberately. States the thesis, the scope, and what "portable" buys. *Sources: `specs/portable-component-model.md` (§0–§1), `build-spec/elite-architecture.md`, `corpus-analysis/06-lessons-from-broader-robotics.md`.* ### 25. The Portable Component Model — the faceplate The core of the proposal. Every active thing — motor, sensor, subsystem, `RobotState`, superstructure — is a **configured transfer function with memory**: `Config` once, then `(State′, Command_out[]) = update(Command_in, Observations)` each tick. Every component presents the same **faceplate** — four serializable PODs and one pure step. The non-obvious payoff: **which channels a component fills *is* its type** (the fill-pattern taxonomy). Emission is a return value, not a side effect; it's a discipline, not a base class; it's the in-process ROS 2 node. Names the shape *faceplate* — a prose term, never a type. *Sources: `specs/portable-component-model.md` (the whole spec).* ## J. The instances ### 26. The portable motor interface — the leaf component The faceplate contract specialized to a leaf actuator: two serializable PODs (`Command`/`MotorState`, named `u`/`x`), nullable payloads, `oneof` control modes, capability tiers, REP-103 units, and a proto3 source-of-truth with a generated ROS bridge. Contrasted with the corpus survey of how other teams talk to motors (ch. 17) so the design choices are visible as choices. *Sources: `specs/portable-motor-interface.md`, `corpus-analysis/05-motor-io-interfaces.md`.* ### 27. The portable swerve interface — the mid-level component A drive subsystem as a component whose children are four module components, each two motors. The five-layer model (WPILib math → `ModuleIO`/`GyroIO` seam → module logic → drive + setpoint generator → `SwerveRequest` vocabulary): "AdvantageKit's seam + CTRE's vocabulary on WPILib's math." Shows seam-granularity as literally *where you draw a component boundary*. *Sources: `specs/portable-swerve-interface.md`, `corpus-analysis/08-drivetrain-as-architecture.md`.* ### 28. `RobotState` and `Superstructure` as components The two higher seams recovered as instances: `RobotState` is "a sensor that fuses" (an observer); the superstructure is a component whose `Command_out` feeds subsystems instead of motors. Why a subsystem and an executive are the *same kind*, why `State` splits into estimate + status above the leaf, and why every level is named `…State`. *Sources: `specs/portable-component-model.md` (§3, §5, §10), `build-spec/subsystems/07-robotstate.md`, `build-spec/subsystems/08-superstructure.md`.* ## K. The dividends and portability ### 29. Telemetry, replay, and tests — the dividends, at every scale What four loggable PODs per component buy: snapshot every component's channels each tick and you have AdvantageKit-grade replay and telemetry for the *entire robot at every altitude*, not just motors; a pure `update` makes every component unit-testable by replaying recorded inputs. The Elite inputs-struct idea (ch. 3 and 16) generalized from leaves to executives. *Sources: `specs/portable-component-model.md` (§2, §4), `build-spec/logging.md`, `build-spec/testing.md`, `build-spec/simulation.md`.* ### 30. Lifecycle and graceful degradation Baking in the discipline FRC most conspicuously skips (see Lessons from Outside). A real component has a ROS-style managed lifecycle; health is a field in `State.status`, not an exception; and the `*IONull` null-object *is* the component in its `fault` state. Degradation becomes a lifecycle transition of the standard shape rather than a special case bolted on. *Sources: `specs/portable-component-model.md` (§7), `corpus-analysis/06-lessons-from-broader-robotics.md` (§5), `alternatives/02-physical-plant-simulation.md`.* ### 31. The ROS bridge and language portability The proof the factoring is right: the faceplate maps to a ROS 2 node with no impedance mismatch (Config↔parameters, Command_in↔topic/goal, State↔topic + feedback, Command_out↔published topics, `update`↔spin). "Keep the message semantics, drop the message transport" — in-process typed calls on the RIO; a real message only on the one inter-process edge (RIO ↔ coprocessor). The proto3 source-of-truth that makes it language-neutral. *Sources: `specs/portable-component-model.md` (§8, §11), `specs/portable-motor-interface.md` (proto3 / ROS bridge).* ## L. Maturity of the proposal ### 32. Open questions and the road to a build recipe The honest close: the League model is a living proposal, not finished doctrine. Two questions are now decided (`Observations` is the children's `State` plus the tick timestamp; execution runs state-up, then commands-down, matching the Elite loop), and the real open ones are named: generic scheduler vs hand-wired composition, threading against the high-rate odometry queue, the command-to-goal adapter for WPILib triggers and autos, and the in-loop allocation discipline — plus the structural gaps the review surfaced (`RobotState`'s dual-graph, the `Subsystem.periodic()` wrap, splitting `Config` retuning from mode switches). What must close before `scaffold-robot`/`add-subsystem` emit to this contract by default. *Sources: `docs/review/portable-component-model-review.md`, `specs/portable-component-model.md` (Open Questions).* --- # Part IV — Scoring Elite Code *(the measurement instrument, and the evidence it works)* *How to score a codebase — yours or anyone's — and trust the number. The rubric, its calibration against real competition results, and one team's four-year climb.* ### 33. The rubric in full The eight-dimension instrument, every 0–4 anchor and the grep/AST cheat-sheet, scored directly — plus the corpus prevalence and cross-validated evidence for *what predicts results and why you must read the code.* *Source: `rubric/rubric.md`.* ### 34. The San Diego scoresheet The rubric run on 24 active San Diego teams against season-matched Statbotics EPA: does better code track winning? Yes — moderately (ρ ≈ 0.55) and unevenly, and the outliers carry the lesson. The full per-team D1–D8 scoresheet and the per-dimension correlation table. *Sources: `survey/sd-frc-final-report.md` (+ `*.csv`, inventories).* ### 35. The Patribots, four years One team (4738) scored season by season with full commit history — a four-season climb 5.0 → 20.0 — the rubric in motion, and the two rules it illustrates: *rewrite in the offseason*, and *great code can still carry a four-year zero.* *Source: `examples/patribots-four-year-scoring.md`.* --- # Appendices - **Appendix A — How We Developed This.** The method behind Part I, as a five-chapter narrative: how the corpus was read (and the *score what's used, not present* rule), the eight-dimension rubric, the novice-to-elite maturity ladder, what the architecture actually predicts against competition results, and the foundation-first build order. *Sources: `examples/methodology.md`, `rubric/rubric.md`, `corpus-analysis/04-novice-to-elite-progression.md`, `survey/sd-frc-final-report.md`, `build-spec/elite-architecture.md`.* - **Appendix B — Glossary and naming decisions.** Faceplate, component, seam, IO line, `u`/`x`, estimate vs status, and why *faceplate* replaced the earlier working name *block*. *Sources: `specs/portable-component-model.md` (§12), `corpus-analysis/03-io-layer-strategy-pattern.md`.* - **Appendix C — Source-document crosswalk.** A table mapping every `knowledge/` file to the chapter(s) that absorb it, so nothing in the corpus is orphaned by the rewrite. - **Appendix D — Reviewing for the seams.** The architecture-first code-review checklist: the five seam invariants a review protects, S0–S3 severity, principles P1–P10, the deterministic review pass, and the auto-fail anti-patterns. *Source: `build-spec/code-review-principles.md`.* --- # Lessons from Outside *(shelved with the appendices — what the broader field treats as table stakes)* *Stepping outside FRC to name what the Elite Architecture is still missing. A single survey chapter today; each lesson will grow its own treatment over time.* ### 1. Lessons from outside FRC The outside-in view: what ROS / Nav2 / MoveIt / autonomous-driving treat as table stakes that FRC skips — graceful degradation, lifecycle, process discipline. Sets up Part III by naming what the Elite Architecture is still missing. *Sources: `corpus-analysis/06-lessons-from-broader-robotics.md`.* ### 2. Spec-in, code-out — generators against the seam The spec-in/code-out generators (RobotBuilder, Tuner X, YAGSL, LLMs) scored against the IO seam — all optimize time-to-drive, not swappability — and the reconciling rule: generate the constants, own the architecture. *Sources: `corpus-analysis/07-code-generators.md`.* --- ## Open decisions for the next pass 1. **Part I ↔ Part II overlap is real and intended — keep it honest.** Several components appear in both (IO seam: ch. 3 + 16–17; world model: ch. 4 + 20; coordination: ch. 5 + 22–23; drivetrain: ch. 6 + 19). The rule is depth: Part I states and motivates, Part II builds. Each Part I seam chapter ends with an explicit "deep dive: Part II ch. X" pointer, and Part II does not re-argue *why* — only *how*. 2. **Chapter granularity — decided: keep single pages.** Ch. 17, 18, 19, 26, and 27 stay as one page each; splitting would churn links for little gain. Revisit only if a page outgrows a single sitting. 3. **Coordination as one chapter or two — decided: keep the split.** Ch. 22 is "build this"; ch. 23 is "know this exists, probably don't build it." Different readers, different registers. The full coordination-ladder graphic lives once, at the close of ch. 23. 4. **Tooling scope.** Whether the wiki documents the scoring/scaffolding *tooling* (`analyze-team`, `scaffold-robot`, the plugin skills in `AGENTS.md`) or stays purely architectural. Currently out of scope. 5. **Chapter numbering — decided: keep the gap.** Chapters 10–14 / section E were absorbed into Appendix A; the numbers are reserved so old citations resolve (noted in "How to read it"). # Part I — The Elite Architecture https://frc-code-scout.jointheleague.org/part-1/ # Part I — The Elite Architecture *The architecture nobody designed and everybody arrived at.* No FRC team set out to define a reference architecture. Yet read enough top codebases and the same shapes appear again and again — a hardware-abstraction seam, a central world model, a coordinator that turns one goal into many setpoints. Part I introduces that convergent architecture: first the shared baseline it grows out of, then the whole thing seen from several angles, then each structural seam in turn, the practices that hang off them, and the legitimate alternatives at the edges. This part explains **what the architecture is** — at low resolution, the whole board before any one piece. It does not open the hood; the engineering detail of each component lives in [Part II](../part-2/). And it does not argue the *method* — how the corpus was read, how to score a team, and how a program climbs the maturity ladder are gathered in the [How We Developed This](../appendices/how-we-developed-this/) appendix, so the narrative here can stay on the architecture itself. ## Chapters **Orientation** 1. [The baseline and the shape](01-baseline-and-shape.md) — command-based as the shared zero, the two joints of coupling, and the two lenses Part I reads the architecture through. 2. [The architecture in five views](02-five-views.md) — the 4+1 view model: the parts and how they relate, the libraries they stack on, the 20 ms loop, the hardware schematic, and two scenarios. **The seams** *(the negative space — the joints between the parts)* 3. [The IO seam — the spine](03-the-io-seam.md) — the Strategy pattern at subsystem granularity. 4. [The state seam — `RobotState`](04-the-state-seam.md) — one fused world model everything shares. 5. [The coordination seam — the superstructure](05-the-coordination-seam.md) — intent vs execution, and the six paradigms. 6. [The drivetrain — the special subsystem](06-the-drivetrain.md) — the only actuator that is also the primary sensor. **The practices around the seams** 7. [Cross-cutting practices](07-cross-cutting-practices.md) — simulation, testing, and logging as dividends of the IO seam. **The frontier** 8. [Alternatives](08-alternatives.md) — legitimate deviations, and the bridge to Part III. 9. [Other advanced topics](09-other-advanced-topics.md) — additive techniques that specialize a seam rather than replace it: state-space/LQR, swerve setpoint generator, threaded odometry, self-check, replay-as-test, neural detection, reactive autonomy, QuestNav. # 1. The baseline and the shape https://frc-code-scout.jointheleague.org/part-1/01-baseline-and-shape/ Every WPILib team starts from the same place. The Elite Architecture is best understood not as a thing teams build from scratch but as a specific set of additions to that shared starting point — so this chapter names the starting point, then names the two lenses the rest of Part I reads the architecture through. A reader who finishes it knows *what* the architecture is made of and *how the next eight chapters look at it.* ## The starting point: two nouns WPILib hands every team the **command-based framework**, which splits robot code into two kinds of thing: - **Subsystems** are the mechanisms the robot *has* — a drivetrain, an elevator, a claw. Each is a class that owns its hardware (the motor and sensor objects) and exposes methods. The scheduler guarantees only one command uses a subsystem at a time, so two pieces of code never fight over the same motor. - **Commands** are the things the robot *does* — drive to a pose, raise the elevator, run a scoring sequence. They *require* one or more subsystems and compose into sequential and parallel groups. `RobotContainer` is the wiring root: it constructs the subsystems, binds them to controller buttons, and selects the autonomous routine. This is real modularity — but it is shallow, and the shape of its coupling is the entire plot of the rest of the book. ```d2 direction: down OI: "RobotContainer (button bindings, auto select)" CMD: "Commands (things the robot does)" SUB: "Subsystems (things the robot has)" HW: "Motor & sensor objects (TalonFX, PhotonCamera, ...)" OI -> CMD CMD -> SUB: calls subsystem methods SUB -> HW: holds concrete devices SUB.style.fill: "#1f3a5a" SUB.style.font-color: "#ffffff" HW.style.fill: "#5a1f1f" HW.style.font-color: "#ffffff" ``` Two joints carry all the coupling. A command holds a *concrete* subsystem reference; a subsystem holds *concrete* motor objects. Almost everything elite teams do is insert a planned boundary at one of those two joints — and the corpus does it in a consistent direction. **Below the line** (between subsystem and devices) teams insert an interface that makes a real motor, a simulated motor, and a replayed motor interchangeable. **Above the line** (between intent and the subsystems) they insert a coordinator and a shared world model, so a button requests a *goal* rather than poking motors, and decisions read from one fused estimate. ## Modularity is a ladder, not a binary The most useful framing from the survey is that decoupling is not on or off. A program climbs: the command/subsystem split WPILib hands you, then an IO layer below it, then a coordinating state machine or graph above it, then a library/robot separation that survives across seasons, and — for the few who need it — message-passing process isolation. The rungs are not adopted in lockstep: a team can have a clean IO layer and no coordinator, or strong logging bolted onto baseline command code. That unevenness is why the architecture is worth seeing from several angles before judging any one team against it. (How to score where a program sits on these rungs is the subject of the [How We Developed This](../appendices/how-we-developed-this/) appendix.) Throughout, we tag each pattern with its dimension in the eight-dimension scoring rubric — D1 through D8 ([ch. 33](../scoring/33-the-rubric.md) / [Appendix A](../appendices/how-we-developed-this/)) — so the chapters and the scores cross-reference. Naming the baseline as the zero is not a way to disparage it. Baseline command-based is *correct*, and for most rungs of most seasons it is enough to win regional matches. The point of fixing it as the reference is to make the additions legible: every later chapter is a specific, motivated answer to a specific pain the baseline leaves open. ## Two ways to see the architecture The same architecture rewards two complementary readings, and Part I gives you both. The first lens is **positive space — the views.** It asks *where the parts are*: which subsystems exist and how they relate, the libraries they stack on, how data flows on each 20 ms tick, and what physical hardware they run on. This is the classic [4+1 view model](02-five-views.md), and it is where Part I starts, because you orient faster by seeing the whole board than by being handed one piece at a time. The second lens is **negative space — the seams.** It asks about *the joints between the parts*: the three planned boundaries — [IO](03-the-io-seam.md), [state](04-the-state-seam.md), and [coordination](05-the-coordination-seam.md) — that a team cuts once so that later capability plugs in instead of forcing a rewrite. The views show you the rooms; the seams show you the load-bearing walls. These two lenses carry the organizing principle of the whole architecture, which is worth stating once up front: **build the seams, defer the payoffs.** Cut the boundaries early and each advanced capability — simulation, tests, replay, vision fusion, smart coordination — becomes an *addition at a known point* rather than a rewrite. That is why a team can grow from a working regional robot to a top-tier program without the offseason rewrites that sink most teams. The next chapter draws the board: the architecture in five views. # 2. The architecture in five views https://frc-code-scout.jointheleague.org/part-1/02-five-views/ No single drawing captures an architecture. A floor plan, a wiring diagram, and a description of how people move through a building are all true at once, and none of them is the others. Software is the same, and the standard way to hold several true pictures at once is Kruchten's **4+1 view model**: four views of the structure — *logical*, *development*, *process*, *physical* — tied together by a fifth, a set of *scenarios* that walk through all four. This chapter renders the Elite Architecture in those five views, at low resolution. It is the whole board before we pick up any one piece. All five describe **positive space** — where the parts are. The chapters after this one describe the **negative space**, the seams between the parts. See the board first. ## Logical view — the parts and how they relate The logical view asks what the major components are and how they talk. "Component" here is the software-engineering sense, not WPILib's: a coherent unit of responsibility, which may be one subsystem or a cluster of them. An elite robot has six that recur. ```d2 direction: down DS: "Driver Station / auto selector (human or routine intent)" CMD: "Command & binding layer (RobotContainer, triggers)" SUP: "Superstructure (coordination — one goal to many setpoints)" SUBS: "Subsystems" { DRIVE: Drive ELEV: Elevator ARM: Arm INTAKE: Intake VIS: Vision } RS: "RobotState (the shared world model)" IO: "IO layer (per-subsystem hardware interface)" HW: Hardware / sim DS -> CMD: button / auto step CMD -> SUP: goal SUP -> SUBS: setpoints SUBS -> IO: device commands IO -> HW: voltages SUBS.DRIVE -> RS: odometry SUBS.VIS -> RS: vision observations RS -> SUP: pose · situation ``` Read it top to bottom and the responsibilities separate cleanly. **Intent** arrives from the driver or an autonomous routine. The **command layer** turns a button or an auto step into a request. The **superstructure** is the coordinator: it takes one robot-wide goal and decides the legal setpoint each subsystem should hold. The **subsystems** each close their own control loop. The **IO layer** is the only thing that touches devices. And off to the side, the **world model** (`RobotState`) is written by the sensing subsystems and read by the coordinator — the one place the robot's belief about the field lives. Communication flows down as *commands* and up as *state*; no component reaches across a layer to poke another's hardware. ## Development view — the libraries it stacks on The development view asks what the code is built *out of* — the library layering a programmer sees at build time. Every FRC robot is a stack, and the elite move is a rule about which layer may depend on which. ```d2 direction: up WPI: "WPILib — HAL, wpimath, command-based the floor; every team builds on it" VENDOR: "Vendor libraries — Phoenix 6 (CTRE), REVLib, PhotonVision / Limelight" IOIMPL: "IO implementations — ElevatorIOTalonFX, ... the one place vendor types are allowed" LOGIC: "Subsystem logic · Superstructure · RobotState no vendor type; depends only on WPILib + the IO interface" WPI -> VENDOR VENDOR -> IOIMPL WPI -> LOGIC IOIMPL -> LOGIC: plugged in via the IO interface { style.stroke-dash: 4 } ``` At the bottom is **WPILib** — the hardware abstraction layer, the `wpimath` geometry and control classes, and the command-based framework. Everyone stands on it. On top of it sit the **vendor libraries**: CTRE's Phoenix 6 for Falcon/Kraken motors and the Pigeon gyro, REVLib for SPARK controllers, PhotonVision or the Limelight library for cameras. A naive robot lets those vendor types spread everywhere. The elite robot confines them: only the **IO implementations** import `com.ctre` or `com.revrobotics`, and the **logic layer** — subsystems, coordination, world model — depends on WPILib and the team's own IO *interface*, never on a vendor. That single dependency rule is what later lets a subsystem run in simulation or against a different motor brand without edits. (Most teams also pull in an ecosystem tier — AdvantageKit or DogLog for logging, PathPlanner or Choreo for paths — and the same rule applies: useful, but kept off the logic's critical dependencies.) ## Process view — what happens every 20 ms The process view asks what runs *when*. An FRC robot is, at heart, one loop that fires every 20 ms (50 Hz), and the most useful thing to trace through it is a single packet of driver intent becoming motion and coming back as measurement. ```d2 direction: right DS: "Driver Station joysticks @ ~50 Hz" NET: radio / network READ: "1. read DS packet + IO.updateInputs()" LOG: "2. log snapshot every input" DECIDE: "3. decide scheduler to goal to setpoints" ACT: "4. actuate IO writes over CAN" PLANT: motors · sensors DS -> NET -> READ READ -> LOG -> DECIDE -> ACT ACT -> PLANT PLANT -> READ: sensor reads, next tick { style.stroke-dash: 4 } ``` Each tick runs the same four steps in the same order: **read** the driver packet and the sensor inputs through the IO layer, **log** that snapshot, **decide** what the mechanisms should do (the scheduler runs commands, a command sets a superstructure goal, the goal fans out to setpoints), and **actuate** by writing through the IO layer onto the CAN bus. The driver never commands a motor directly; the packet sets *intent*, and the loop turns intent into voltage. Most of the robot is single-threaded by design — predictability beats parallelism here — with two deliberate exceptions: high-rate odometry can run on its own 250 Hz thread, and vision runs asynchronously on a coprocessor and pushes timestamped measurements in when they are ready. ## Physical view — what connects to what The physical view is the one most software writeups skip, and on a robot it is indispensable: the code runs on real electronics wired in a specific topology, and the boundaries in the logical view often mirror boundaries in the wiring. This is the robot as an electrician sees it. ```d2 direction: down DSL: "Driver Station laptop (field wall / pit)" RADIO: "Robot radio (Wi-Fi to the field)" SWITCH: Ethernet switch / PoE RIO: "roboRIO runs the robot program" PDH: "Power Distribution Hub battery to everything" LL: "Limelight smart camera" OPI: "Coprocessor Orange Pi · PhotonVision" CANrio: "CAN bus (roboRIO)" { PIGEON: Pigeon 2 — gyro MA: TalonFX — arm MI: SPARK MAX — intake PDHc: Power Distribution Hub } CANivore: "CANivore bus (USB-CAN)" { DRV: 4x TalonFX — drive STR: 4x TalonFX — steer ENC: 4x CANcoder } LOCAL: "roboRIO DIO / PWM / Analog beam breaks · limit switches · servos" DSL -> RADIO: Wi-Fi RADIO -> SWITCH SWITCH -> RIO: Ethernet SWITCH -> LL: Ethernet SWITCH -> OPI: Ethernet RIO -> CANrio: CAN RIO -> CANivore: USB RIO -> LOCAL: wires PDH -> RIO: 12 V { style.stroke-dash: 4 } ``` The **roboRIO** is the brain and the only thing running team code. Everything actuated or sensed over **CAN** hangs off one daisy-chained, terminated bus — motor controllers, the Pigeon gyro, CANcoders, the power distribution hub — and strong teams put the swerve modules on a **second** CAN bus (a CTRE CANivore over USB) to get the bandwidth for high-rate odometry. **Cameras and coprocessors** are not on CAN at all; they reach the roboRIO over **Ethernet** through a switch, and talk to the field through the **radio**. Simple sensors — beam breaks, limit switches, servos — wire straight into the roboRIO's digital, PWM, and analog ports. Notice the mirror: the IO seam in the code falls almost exactly on the CAN boundary in the wiring — the things behind an `XxxIO` interface are, physically, the things on the bus. ## Scenarios — the views in motion The fifth view is the others put in motion: a concrete task traced through all four. Two are enough to show the shape. **A teleop score.** The operator presses *score L4*. A driver packet (process view) carries the button to the roboRIO; the binding layer (logical view) turns it into a goal handed to the superstructure, which sequences setpoints — clear the frame, raise the elevator, then release — and the subsystems drive those setpoints through their IO layer onto the CAN bus (physical view), all in code that imports no vendor type above the IO line (development view). The whole path, button to motor voltage, completes inside one or a few 20 ms ticks. **An autonomous routine.** No driver packet now; intent comes from a path-follower running as a command. It reads the robot's pose from `RobotState` — fused from CANivore odometry and Ethernet vision corrections — computes a chassis speed, and feeds the drivetrain the same setpoint path a teleop request would. Same components, same loop, same wiring; only the source of intent changed. That interchangeability of intent sources is the architecture working as designed. Five views, one architecture. They show you where everything *is*. The next chapters turn the picture inside out and look at the **seams** — the joints between these parts, where the architecture's real leverage lives. One number frames those chapters. The seams are the elite target, not the median: measured across the 55-repo season index, the IO seam appears in 24 teams, a `RobotState` world model in 26, and a real coordinator in 23 — but **all three together in only 10 teams (18%)**. The individual parts are common; assembling the full set of seams is what separates the top tier. We start with the spine: [the IO seam](03-the-io-seam.md). # 3. The IO seam — the spine https://frc-code-scout.jointheleague.org/part-1/03-the-io-seam/ The IO seam is the single most widely shared idea in serious FRC code. It appears in 24 of the 55 season repos in the corpus index (44%) and is the default rather than the exception among the strong ones; the earlier 37-team hand survey found it in roughly two-thirds of the strong Java and Kotlin codebases. It is the spine the rest of the architecture hangs from, so it gets named first. ## The problem it solves WPILib hands every team a subsystem that owns its motors directly: the drivetrain holds `TalonFX` objects, calls `setVoltage` on them, and reads `getPosition` back. That works, and it is where every team starts. The coupling it leaves is the problem — the subsystem's logic is welded to specific hardware. You cannot run it without the physical robot plugged in, you cannot swap a motor controller without editing the logic, and you cannot replay a match through the code because the sensor reads come straight off the CAN bus. The IO seam breaks that weld with one move: **insert an interface between the subsystem's logic and its physical devices.** The subsystem talks to the interface; concrete implementations talk to hardware, to a physics simulation, or to nothing at all. The logic no longer knows or cares which. ## What it really is: Strategy "IO layer" is FRC's house name for it, but the pattern has an older, more precise one: it is the **Strategy pattern applied at subsystem granularity.** `ElevatorIO` is the strategy interface; `ElevatorIOTalonFX`, `ElevatorIOSim`, and a no-op replay variant are interchangeable strategies; the subsystem holds one of them without knowing which. ```d2 direction: down SUB: "Elevator subsystem (logic only — no vendor type)" IO: "interface ElevatorIO (updateInputs + commands)" REAL: "ElevatorIOTalonFX (real hardware)" SIM: "ElevatorIOSim (physics model)" REPLAY: "ElevatorIO { } (no-op; log feeds inputs)" SUB -> IO IO -> REAL IO -> SIM IO -> REPLAY ``` The value is the independence: a team writes the subsystem's logic *once* against the interface, then swaps the strategy underneath — real hardware during a match, a physics model on a laptop, a log-replay stub afterward, a different motor vendor next season — and none of those swaps touches the interface or the subsystem. Selection happens in exactly one place, keyed off the robot's run mode — the three run modes, **REAL**, **SIM**, and **REPLAY**, are chosen at that single point, with the mechanics in [Part II ch. 15](../part-2/15-control-path.md) — so "run on the real robot" versus "run on a laptop" turns on a single line. Two neighbors are worth naming so they don't get confused with it. A **factory method** (`Elevator.create()`) is the *selection* step that picks the strategy. A **null object** (a do-nothing implementation) is a full member of the strategy family whose methods are deliberately empty, so a subsystem with disconnected hardware runs as a safe no-op instead of crashing. ## Why the pattern won Three payoffs fall out of that one interface, and they explain why a pattern once seen only in powerhouse code is now common at regionals: - **Simulation is free.** Swap the real implementation for the sim one — one line at construction — and the entire subsystem runs on a laptop with no robot. For a team with ten programmers and one robot on the cart, this is the whole game. - **Unit testing becomes possible** — the seam's deferred dividend. Because a subsystem can be built with a sim implementation, you can drive it to completion on CI (continuous integration) and assert on the result. Almost no FRC teams test robot code at all, and the IO seam is what makes it mechanically possible. - **Hardware swaps and replay stay local.** Changing a motor controller, supporting a practice robot, or replaying a recorded match touches one IO file — or, with AdvantageKit, none, because the log feeds the existing code. ## Reading it in the corpus Those 24 corpus teams come with a confirmation worth trusting: *every* team that builds an IO interface also has the logged `Inputs` struct (zero exceptions). But naming misleads — the hardware implementation is named *by device* (`ElevatorIOTalonFX`, `GyroIOPigeon2`), not "Real." Literal `*IOReal` appears in only about 5 teams, so the robust signal is **an `interface *IO` with two or more implementations, one of them a sim** — not a filename grep. One distinction the corpus forces: an IO *directory* of concrete hardware wrappers is not an IO *layer*. There must be an actual interface with swappable implementations behind it; tidy hardware encapsulation alone is organization, not dependency inversion. The seam has internal decisions — where the control loop sits, whether reads come through an inputs struct or plain getters, when to collapse per-subsystem interfaces into one generic base — but those are mechanics, and they live in [Part II](../part-2/16-hardware-abstraction.md). What matters here is the shape: one interface, interchangeable strategies, selected in one place, swappable over time without touching the logic above it. Next: the seam that the IO layer feeds — [the state seam](04-the-state-seam.md). # 4. The state seam — RobotState https://frc-code-scout.jointheleague.org/part-1/04-the-state-seam/ The second seam is a single object that owns the robot's best belief about the world — where it is on the field, and later its game-piece and mechanism state. Sensors *write* to it; decisions, pathing, and autonomous *read* from it. One fused estimate that everything shares, rather than each subsystem keeping its own guess. ## Sensors in, belief out `RobotState` has no hardware and no control loop. Its "IO" is purely informational: observations in, a fused pose out. ```d2 direction: right DR: Drive subsystem VIS: Vision subsystem RS: "RobotState pose estimator + time buffer" CONS: Auto / Path / Aim / Superstructure DR -> RS: odometry (fast) VIS -> RS: vision obs (delayed) RS -> CONS: getPose() RS -> VIS: sampleAt(t) ``` Two streams arrive at different rates and latencies. **Odometry** is fast and continuous but drifts. **Vision** is accurate but sparse and *delayed* — an AprilTag frame is timestamped in the past. `RobotState` reconciles them with a time-interpolating buffer: it keeps a couple of seconds of timestamped odometry poses, so when a delayed vision measurement arrives it can rewind to where the robot *was* at that timestamp, blend the correction in by its trust weight, and replay odometry forward to now. The mechanics of that blend are [Part II ch. 20](../part-2/20-the-world-model.md); what matters here is that the reconciliation happens in one place. ## Why centralizing it is the architectural move Pose estimation itself is not the elite signal — it is the floor. WPILib's `SwerveDrivePoseEstimator` already does odometry-plus-vision fusion, and `addVisionMeasurement` appears in 50 of 55 corpus teams. The common shape is a pose estimator owned *privately by the drivetrain*, with vision reaching into the drive subsystem to correct it. The architectural move is pulling that estimate out of the drivetrain into its own object, so vision, pathfinding, and autonomous all read one consistent world model instead of reaching into the drive class. That is the difference the rubric marks between D7 level 2 (a pose estimate exists) and level 4 (a world model is the architecture): | Level | Shape | Who | |---|---|---| | L2 | a `SwerveDrivePoseEstimator` owned privately by Drive; `addVisionMeasurement` called from vision | most teams | | L3 | + std-dev / ambiguity **rejection** before fusing | 3061, 254 | | L4 | a dedicated `RobotState` owning the estimator + time buffer, decoupled from Drive; a world model of pose + game-piece + mechanism state | 6328, 254 | The levels are cumulative, not a partition — a team at L4 also does the L3 rejection, which is why 254 appears in both rows. At L4 the object stops being "pose" and becomes the robot's belief about everything — 6328's `RobotState` also carries game-piece observations, robot velocity, and even mechanism extension, so "where am I and what's the situation" lives in one inspectable, logged place. ## The cleanest class on the robot Because `RobotState` takes plain data — a `Pose2d`, a `SwerveModulePosition[]`, a timestamp — and returns a fused estimate, it is pure `edu.wpi.first.math` geometry with no vendor type and no IO implementation anywhere in it. Grep its imports and you will not find a `TalonFX` or a `PhotonCamera`. If one appears, the seam has leaked — a subsystem handed it a device handle instead of an observation. That purity has a consequence the corpus repeatedly fails to collect: `RobotState` is the **most unit-testable class on the robot** and almost the **least tested.** There is nothing to mock — feed it a second of straight-line odometry, assert the pose integrated to one meter; feed it a tight vision observation, assert the estimate pulled toward it. No HAL, no robot, no other subsystem. It is the highest-leverage test most teams are not writing, and it falls straight out of building the seam at all. The state seam and the IO seam together give the robot a body and a sense of place. The third seam decides what to do with them: [the coordination seam](05-the-coordination-seam.md). # 5. The coordination seam — the superstructure https://frc-code-scout.jointheleague.org/part-1/05-the-coordination-seam/ The third seam turns one robot-wide *goal* into a coordinated set of subsystem setpoints, through a single transition function that is allowed to *reorder or reject* a transition for safety. A button asks for `SCORE_L4`; the superstructure decides the legal sequence — clear the frame, raise the elevator, then score — that realizes it. It is the one object that sees every mechanism at once, so the knowledge that "the arm must clear before the elevator rises" lives in exactly one place instead of scattered across subsystems. A note on names: throughout the book, lowercase *coordinator* is the role, *superstructure* is FRC's house name for it, and backticked `Superstructure` marks the concrete class teams give it. ## Intent versus execution The split that defines the seam is **intent versus execution.** A caller expresses *what it wants* — a goal — and walks away. The superstructure owns *how each mechanism gets there* — the legal, sequenced setpoints. There is no control loop here; each subsystem still closes its own loop through its IO. The superstructure only decides which setpoint each subsystem should hold right now. ```d2 direction: right start: "" { shape: circle } STOW INTAKE SCORE_L4: "SCORE_L4 (guarded: arm clears the frame before elevator raises)" start -> STOW STOW -> INTAKE: request INTAKE INTAKE -> STOW: has piece STOW -> SCORE_L4: request SCORE_L4 SCORE_L4 -> STOW: scored ``` Because only the transition function writes setpoints, a caller *cannot* drive a mechanism into an illegal configuration. The operator binding never commands a motor — it requests a goal, and the same goal is reusable as an autonomous action. Re-tuning the scoring sequence becomes a one-place change. This is also where kinematic interlocks belong: the guard that refuses to let two mechanisms occupy the same space lives in the coordinator, not smeared across the subsystems that can't see each other. ## This is where teams diverge The first two seams are near-consensus in their shape. Coordination is the richest point of architectural divergence in FRC — there are six recognizable paradigms, and the rubric's D2 ladder orders them by how much the transition logic is *data* rather than *code*: | Level | Shape | Example | |---|---|---| | 1 | command composition — sequential/parallel groups, no coordinator | most teams | | 2 | **wanted/current** FSM (defined in [ch. 22](../part-2/22-coordination-state-machines.md)), distributed (one per subsystem + a top one) | 2910, 4099 | | 2 | **centralized** `RobotManager` — one FSM, dumb subsystems | 581, 3128 | | 3 | a `Superstructure` coordinator separating intent from execution, with kinematic safety | 254, 3476, 6328 | | 4 | **state graph** — transitions are data, pathfind through legal states | ~5 teams; see below | The full six include two more at the frontier: the **behavior tree** borrowed from game AI (3015 ship a complete runtime with a visual editor), and **inter-process message passing** (971's custom robotics OS, where each subsystem is a separate process behind a typed message contract). The seam's primary deep dive is [Part II ch. 22](../part-2/22-coordination-state-machines.md), which builds the FSM and transition-function mechanics; [Part II ch. 23](../part-2/23-coordination-graphs-trees.md) takes the "transitions outgrew the FSM" tools — the graph and tree forms — apart, and the message-bus extreme is covered in [Lessons from Outside](../appendices/lessons-from-outside/01-lessons-from-outside.md). The corpus shows `Superstructure` in 22 of the 55 season repos and a generic `*StateMachine` in 12. At the ceiling, explicit state-graph or transition types appear in about five teams (190, 254, 2910, 3476, 5026); 6328's graph shows up separately through the `jgrapht` dependency marker (three teams); and genuine A\* search over a discretized configuration space is 254 alone. The other frontier markers — a `WantedState` enum, a behavior-tree runtime — are similarly rare. A `Superstructure` at level 3, optionally backed by a state machine, is the common elite path; the graph and tree are rare. ## The level-1-in-level-3-clothing trap The seam's name is easy to fake. A class called `Superstructure` that only holds subsystem references and exposes manual jog buttons is baseline command composition wearing a level-3 name — no goal enum, no transition function, no interlock. Before crediting real coordination, confirm there is an actual goal-request API and a single transition function that all transitions route through. The diagnostic that separates the real thing from the impostor: *is intent separated from execution, and does one function own every transition?* A clean coordination seam is also naturally vendor-free — it imports the subsystems' public `setGoal`/`setState` API, never their IO implementations and never a `com.ctre` type. A vendor import here is a loud signal that a subsystem has leaked its hardware upward. With the three seams named, the next chapter covers the one subsystem that behaves unlike all the others and earns its own treatment even in the overview: [the drivetrain](06-the-drivetrain.md). # 6. The drivetrain — the special subsystem https://frc-code-scout.jointheleague.org/part-1/06-the-drivetrain/ Ask "what is a drivetrain" and the easy answer is "the thing that makes the robot move." The corpus shows that undersells it. Every other subsystem is *either* an actuator (an arm moves) *or* a sensor (a camera measures). The drivetrain is the only one that is **both at once** — and what it measures is the single value the rest of the robot depends on most: where it is on the field. That is why it earns its own chapter even in the overview. ## Three roles in one class A drivetrain is simultaneously three architectural things, and a team's sophistication is largely *which of the three it separates into its own layer*: ```d2 direction: down DRIVE: Drivetrain { ANCHOR: "3 — World-model anchor owns the pose estimator" SUB: "2 — Subsystem the command target" DEV: "1 — Device / plant 4 modules + gyro" ANCHOR -- SUB SUB -- DEV } HW: hardware / CTRE swerve WORLD: Auto · Aim · Vision DRIVE.DEV -> HW: below the IO line { style.stroke-dash: 4 } DRIVE.ANCHOR -> WORLD: Pose { style.stroke-dash: 4 } ``` A novice team fuses all three into one `Drivetrain extends SubsystemBase` god-class. An elite team splits them: the device below an IO seam, the subsystem above it, the pose estimator inside the subsystem. The whole build is, in effect, "give each of the three roles its own altitude." ## The one subsystem you can assume exists Across 55 season repos, a drivetrain class appears in 52 (94%), lives in a `/subsystems/` directory in 85%, and is modeled as a command-based subsystem in 81%. There is no competitive robot in the corpus whose drivetrain is *not* a subsystem — it is the load-bearing example of the command-based pattern. And it is overwhelmingly swerve: differential drive survives almost exclusively in Ri3D (Robot in 3 Days demo builds) and rookie code, with true swerve prevalence around 85–90% once you count the CTRE-generated robots a class-name grep misses. For a competitive team in this era, "drivetrain" means "swerve" by default. ## The spectrum: generated default versus owned seam Classifying swerve drivetrains by primary architecture reveals where the seam discipline actually lives: | Style | share | what it means | |---|---|---| | CTRE-generated, used ≈ as-is | 48% | the generated `CommandSwerveDrivetrain` *is* the subsystem — fast, vendor-locked | | CTRE wrapped below an owned IO seam | 14% | the 254/2910 pattern — generated device demoted below a `DriveIO` | | hand-rolled monolithic | 12% | one big `Drivetrain extends SubsystemBase` | | per-module `ModuleIO`/`GyroIO` | 12% | built from motors, fine-grained seam | | YAGSL / REV template / differential | ~14% | config black box or legacy | Two readings: about **63% of teams sit on CTRE's generated drivetrain** in some form — it has become the de-facto default drive base of FRC — while only about **27% own a real IO seam.** The seam the rubric rewards is still a minority practice, concentrated in the strongest teams. The elite move reads straight off the package listing. In 254's and 2910's `drive/` directories the three roles are split into separate files, and the CTRE-generated `CommandSwerveDrivetrain` is **demoted to a device** — it does not `implements Subsystem`; it is wrapped by a `DriveIOHardware` so `com.ctre` stops at the seam and never reaches the subsystem. They ingest the generator's constants but own the architecture — "generate the numbers, own the architecture." There are two altitudes to cut the seam (per-module when you build from motors, per-drivetrain when you wrap a vendor's swerve); both are the same seam at different heights, a distinction [Part II ch. 19](../part-2/19-the-drivetrain-subsystem.md) develops. ## Pose is the proof The claim that the drivetrain is the world-model anchor is measurable. In the CTRE drive state struct, the `Pose` field is read **682 times across the corpus — more than any actuator field on any subsystem.** Auto, aiming, and vision all consume it. The drivetrain's most important output is not its motion command but its state estimate: *where am I*. That single number is why the drivetrain feeds the state seam of [ch. 4](04-the-state-seam.md), and why "what is a drivetrain" is finally a question about where you draw your boundaries. That closes the architectural core. The next section steps back to the practices that hang off these seams — [simulation, testing, and logging](07-cross-cutting-practices.md). # 7. Cross-cutting practices https://frc-code-scout.jointheleague.org/part-1/07-cross-cutting-practices/ Three practices separate engineering culture from cargo cult, and all three are **dividends of the IO seam** ([ch. 3](03-the-io-seam.md)) rather than independent features. Build the seam and they become available; the work is collecting them. This chapter is what they are and why teams leave most of them on the floor. The harnesses and code are [Part II territory](../part-2/) — here, the shape. ## Simulation (D3) Simulation runs your *actual* robot code with the physical robot replaced by a model. Because the subsystem holds an `XxxIO`, the sim is just a different implementation of it — `XxxIOSim` — selected at the one run-mode branch. The value scales with fidelity, and that scale is the D3 ladder: | D3 | Fidelity | What you get | |---|---|---| | 1 | `simulationPeriodic` stub echoing setpoints | "it runs without a robot" | | 2 | WPILib mechanism sims (`ElevatorSim`, `FlywheelSim`…) in the `IOSim` | the controller actually has to converge | | 3 | whole-robot dynamics (maple-sim) + vision sim | the sim can surprise you | | 4 | deterministic replay of a real match | re-run the actual robot on actual data | The threshold that matters is level 2: a sim that echoes setpoints only proves the code doesn't crash; a sim with real physics constants (measured mass, MOI — moment of inertia — and gearing) makes the controller earn its convergence and can reveal an overshoot or an unstable loop before the robot exists. The corpus mindset is to treat sim as a *separate robot with its own tuning* — same code path, different constants — which is exactly what makes it trustworthy. ## Testing (D4) A test constructs part of the robot, drives it, and asserts what it did. The defining FRC fact: almost no one does it. D4 is the rarest, most discriminating marker in the corpus — most teams score 0 — and it barely correlates with winning ([What the architecture predicts](../appendices/how-we-developed-this/04-what-it-predicts.md)). That is the point. Tests don't put points on the board; they are the clearest signal of software-engineering culture and the thing that lets a program move fast without breaking itself. The reason it's rare is that it has a prerequisite — the IO seam — and the reason it's *possible* is that you already built it. The key idea: **the `XxxIO` interface and its `XxxIOSim` are the test double.** No Mockito, no mocking framework — to test a subsystem you hand it the sim implementation; to test a command or the superstructure, you build the subsystems on sim one level down. The highest-value test is the interlock test: build several subsystems in sim, request a dangerous goal, and assert the safe ordering held — the failure that physically breaks robots, caught on a laptop. ## Logging (D5) "When the robot misbehaves between two qualifier matches, with no laptop attached, how do we know why?" Logging is the answer, and the bar is not "print some numbers" — it's "reconstruct the failure afterward." That reconstruction is impossible with `println` and trivial with a logged inputs struct, which is why the seam captures every hardware reading in one place. | D5 | Stack | What it is | |---|---|---| | 0–1 | `println` / `SmartDashboard` | live-only, not recorded | | 2 | DogLog / Epilogue | structured logging to a recorded file + NetworkTables | | 3 | AdvantageKit | `@AutoLog` inputs + `Logger.processInputs` across every subsystem | | 4 | replay + diagnostics | log replay exercised; self-check fault reporting | Logging is the one advanced practice that has gone near-universal among serious teams — AdvantageKit appears in 26 of the 55 season repos, with Epilogue, DogLog, and URCL trailing. The jump that matters is level 1 → 2: from live-only dashboard values to a *recorded* log you can open after the match. And the decision between stacks is real, not a default — AdvantageKit buys deterministic replay at the cost of run-mode plumbing and strict IO discipline; DogLog buys telemetry now and skips replay. Because both consume the same `Inputs` struct, the choice is deferrable and the migration touches `Robot.java`, not the subsystems. ## The dividend almost no one collects The three practices chain: filling `IOSim` (an afternoon) unlocks both unit tests and — once the `REPLAY` run mode from [ch. 3](03-the-io-seam.md) is wired — deterministic replay of a real match through unchanged code. Read the ch. 3 numbers as a dividend ledger: 24 teams built the seam, all 24 hold the inputs struct replay consumes, and about **one** actually ships a replay variant. This is ch. 1's "build the seams, defer the payoffs" seen from the collection side — the foundation already paid for the dividend, and collecting it is the clearest marker of real software culture, the subject of [Foundation-first](../appendices/how-we-developed-this/05-foundation-first.md). With the seams and their practices in hand, the last two chapters look outward, starting with the legitimate deviations: [alternatives](08-alternatives.md). # 8. Alternatives — legitimate deviations https://frc-code-scout.jointheleague.org/part-1/08-alternatives/ The Elite Architecture is opinionated on purpose — it teaches one foundation-first path so a team can grow without rewrites. That path is the *default*, not the *only* correct design. This chapter catalogs the legitimate deviations: patterns that are not the corpus default and not what the build spec recommends out of the box, but that are sound, defensible, and worth reaching for in the right situation. An entry earns its place by being **sound** (not a beginner anti-pattern wearing a clever name), **uncommon** (if it were the norm it would be in the build spec), **situational** (a real case where it's the better call *and* a real case where it's over-engineering), and **guard-railed** (it comes with the conditions that keep it from degrading into the anti-pattern it neighbors). None of these changes a rubric score; they're a reference for recognizing a pattern and judging whether it was applied *with* its guardrails or without them. ## Capability-typed devices The build spec draws the seam at the *subsystem* (`ElevatorIO`). This alternative draws a second, lower seam at the *device*: interfaces named for a **capability** — `PositionMotor`, `VelocityMotor` — never a vendor (`ITalonMotor`), with a single hardware object that constructs every device, owns its configuration, and hands each subsystem the narrow interface it needs. The insight is that once you've decided a motor's job, you've decided its control strategy, so the *commands* collapse to a vendor-neutral vocabulary and only *configuration* stays vendor-shaped — so you abstract the command surface and seal the config inside the implementation. *Pays off* when several mechanically similar mechanisms can share one implementation, or you have a real multi-vendor or multi-robot future. *Over-engineering* for a single mechanism where the subsystem-level seam already says everything. The corpus shows a device-level `MotorIO` in about ten of the 55 season repos, but almost all leak vendor types; the vendor-clean form is rare, which is why it's an alternative. It is also the most direct on-ramp to [Part III](../part-3/), whose motor interface is this idea made portable. ## Physical-plant simulation `RobotState` is the robot's *estimate* of the world. This alternative materializes its dual: a **plant** holding the world's *true* state. Settable truth plus a fidelity dial split the simulator into three independently testable models — dynamics, observation, and estimator — and let a test assert on **estimation error**, the one thing you can never measure on hardware. The control law stays on one side of the seam, so nothing is duplicated. *Pays off* when you want sim to be a test rather than a demo — to prove the estimator converges and the controller is stable before the robot exists. *Over-engineering* when the sim only echoes setpoints and no one asserts on it. Both halves already exist unnamed in the corpus (an estimate in 26 teams, truth models wherever physics forces them); what's missing is the estimate-versus-truth assertion that turns sim into verification — the D3-high / D4-low gap the rubric flags as the most actionable team finding. ## State-graph coordination The build spec's superstructure hand-codes the safe transition sequences. This alternative models the superstructure as a **state graph** and *searches* for a collision-free path — at the far end, A\* over a discretized configuration space, where "obstacles" are self-collision regions. The prize is interlocks-as-edge-existence (declare safety once, locally) and verifiable coordination (a graph can be exhaustively checked that every transition terminates safely, which hand-coded sequences cannot). *Pays off* when the N² transitions (every pair hand-coded) get error-prone and you want provable safety; teams already run anytime A\* (it returns the best path found so far) on the drivetrain, so this is the same algorithm one layer up. *Over-engineering* before a team has hit the limits of a finite-state machine. Established but uncommon: explicit state-graph or transition types appear in about five teams (190, 254, 2910, 3476, 5026), 6328's graph is caught separately by its JGraphT dependency, and genuine A\* over a discretized configuration space is 254 alone. ## Behavior trees The reactive partner to the state graph: a re-ticked tree returning SUCCESS / FAILURE / RUNNING every cycle, so preemption and recovery fall out of *structure* rather than hand-wired transitions. It is the game-AI and Nav2 answer to "what do I do *now*," and the natural response to the question a state machine eventually raises — what happens when the transitions themselves get complicated. *Pays off* for deeply nested, priority-driven, reactive decision logic. *Over-engineering* before that complexity is felt. Explicit behavior trees are nearly absent in FRC (3015 is the standout, with a full runtime and a visual editor), while the command-group cousin (`SequentialCommandGroup`) is universal — the delta is using it as a top-level *reactive brain* rather than a fixed script. --- Two of these — capability-typed devices and the plant — point directly at the proposal in [Part III](../part-3/), where one component shape spans motors, sensors, subsystems, and executives, and truth-versus-estimate is built into the model. The other two are the D2 ceiling the Elite Architecture already gestures at. One boundary is worth drawing precisely before Part I's final chapter. Everything in this chapter is an **alternative**: a different way to build one of the seams themselves, adopted *instead of* the default. The next chapter collects the opposite category — **additive** techniques that leave every seam exactly as it is and attach new capability at one of them, so they can be added or removed without restructuring the robot. Alternatives replace a seam's design; additions specialize it. With that distinction in hand: [other advanced topics](09-other-advanced-topics.md). # 9. Other advanced topics https://frc-code-scout.jointheleague.org/part-1/09-other-advanced-topics/ Where [chapter 8](08-alternatives.md) cataloged the *alternatives* — different ways to build a seam, with the graph and tree forms taken apart in [Part II ch. 23](../part-2/23-coordination-graphs-trees.md) — this chapter catalogs the *additions*: eight techniques that top teams layer onto the elite architecture without restructuring it. Each attaches at an existing seam, and each earns its place only when the payoff is real. They range from near-standard on serious swerve to genuinely rare. As in chapter 8, each entry gets what it is, when it pays off, and where it attaches; the mechanics live in Part II. ## State-space & LQR control State-space control replaces a hand-tuned PID loop with an optimal controller derived from the mechanism's physics: model the mechanism as a linear system, compute the feedback gain with a **Linear-Quadratic Regulator (LQR)**, and estimate the unmeasured state with a Kalman filter. The draw is principled gains computed from physical constants instead of trial and error, plus an observer that rejects sensor noise rather than fighting it. It is the rarest technique in this chapter, because feedforward plus PID gets most mechanisms most of the benefit. It drops in as the closed-loop controller *inside* a subsystem — below the coordination layer, above the IO line, with the IO layer unchanged. Mechanics in [the control path](../part-2/15-control-path.md) and [subsystem archetypes](../part-2/18-subsystem-archetypes.md). ## Swerve setpoint generator A naive swerve drive commands module states the wheels cannot physically reach within one 50 Hz tick; the result is skid, degraded odometry, and tipping under hard acceleration. A setpoint generator clamps each loop's command to the closest feasible one. Drive a swerve robot hard and it pays for itself immediately — cleaner acceleration, and odometry that stays honest because the wheels actually track their commands. Originated by 254 and now packaged inside PathPlannerLib (and spreading elsewhere), it is trending toward standard. It sits between the chassis-speed request and the module IO calls inside the Drive subsystem; [the drivetrain subsystem](../part-2/19-the-drivetrain-subsystem.md) carries the details. ## High-frequency threaded odometry The main loop runs at 50 Hz, but a swerve pose estimate sharpens considerably when a dedicated thread samples the drive encoders and gyro at 250 Hz (CANivore) / ~100 Hz (RIO CAN bus) and feeds every timestamped sample into the estimator. Nearly every elite swerve robot collects this benefit — the pattern is baked into the AdvantageKit swerve template. The thread lives at the boundary between the drivetrain and the world model, and the sampling stays *below* the IO line, so no vendor type leaks upward. See [the drivetrain subsystem](../part-2/19-the-drivetrain-subsystem.md) and [the world model](../part-2/20-the-world-model.md). ## Self-check & fault diagnostics An operational technique rather than an algorithmic one: a `systemCheck` command drives each subsystem through a scripted range of motion in the pit, and a fault reporter polls every device's fault flags and surfaces them on the dashboard before the match. It turns "the robot is acting weird" into a named, actionable fault a student can fix — rare, which is exactly why it differentiates: [ch. 7](07-cross-cutting-practices.md) marks it as the top rung of the diagnostics ladder. It attaches to the lifecycle, running in disabled mode and reading device faults through the same IO inputs the subsystems already expose (see also [Part III ch. 30](../part-3/30-lifecycle-degradation.md)). ## Replay as a regression test AdvantageKit's deterministic replay — feed a recorded match's inputs back through the code and get identical outputs — is usually framed as a debugging tool, but it doubles as a regression fixture: check a logged match into the repo, and CI can assert the robot still makes the same decisions after a refactor. Any team already logging with AdvantageKit has the infrastructure; almost none collects the dividend ([ch. 7](07-cross-cutting-practices.md)'s point exactly), which makes it a near-free win. It builds on the same inputs-struct logging as the sim-based tests and attaches to the test pipeline. The replay mechanics are in [Part II ch. 15](../part-2/15-control-path.md); Part III [ch. 29](../part-3/29-telemetry-replay-tests.md) generalizes the idea. ## Neural game-piece detection Beyond AprilTag pose estimation, a neural detector on a Limelight or PhotonVision coprocessor reports the bearing — and sometimes range — to game pieces, so routines can drive to the best available target. The detector itself is common among serious teams; the differentiating part is the architecture on top: drive-to-piece logic that rejects bad detections, filters flicker, and degrades gracefully when nothing is seen rather than lunging at a phantom. The payoff lands in autonomous and driver-assist teleop. Architecturally it is just another vision IO — observations crossing the line as plain numbers, the vendor SDK below it — feeding coordination-layer logic; [vision systems](../part-2/21-vision-systems.md) has the mechanics. ## Reactive / adaptive autonomy Most autos are fixed scripts that assume the world cooperates. A reactive auto re-decides on what it actually senses: skip a pickup when the piece is missing, take the open scoring branch, abort and reposition rather than stall. In practice it is PathPlanner conditional paths and event markers plus a small decision layer reading the world model. Over a season, surviving a missed first piece is the difference between a reliable ten points and an occasional zero. It lives in the coordination layer, reading [the world model](../part-2/20-the-world-model.md) to branch its command sequence; [coordination with graphs and trees](../part-2/23-coordination-graphs-trees.md) formalizes the same ideas. ## QuestNav (VR-headset localization) A Meta Quest headset is a cheap, extremely good inside-out 6-DOF tracker — it has to be, to render VR without inducing nausea. QuestNav mounts one on the robot and streams its pose over NetworkTables as a high-rate, low-drift odometry source fused with AprilTag vision. The newest technique here — one of the 55 season repos uses it, genuinely emerging across the 2025–2026 seasons — it is worth carrying where pose accuracy and update rate outweigh the cost and fragility of a consumer headset on a robot. Architecturally it is one more observation source feeding the same pose estimator: a new vision IO, with the headset SDK below the line like any other sensor. See [vision systems](../part-2/21-vision-systems.md) and [the world model](../part-2/20-the-world-model.md). --- Adopt these from the seam outward: start from the seam each one attaches to, add it when its payoff is real for your robot, and let the three-seam foundation carry the weight underneath. That closes Part I — the baseline, the views, the seams, the practices, the deviations, and the additions. [Part II](../part-2/) opens the hood. # Part II — Anatomy of the Elite Architecture https://frc-code-scout.jointheleague.org/part-2/ # Part II — Anatomy of the Elite Architecture *The hood comes off.* [Part I](../part-1/) established what the Elite Architecture is, why to believe it, and how a program grows into it — at low resolution, deferring every mechanism. Part II is the engineering reference. Each chapter takes one major component, opens it, and shows the contracts, the decisions, the real variants in the corpus, and what to build. Code is quoted throughout from the public codebases the architecture was read from — **to study the technique, not to copy.** The dividing line with Part I is depth: Part I motivates, Part II builds. Where a Part I chapter named a seam and pointed here, this is the page it pointed to. > **Which corpus, which N.** Part II cites counts from three related datasets: a **37-team hand > survey** (close reading, one team at a time), a **55-repo season index** (one repository per team > for the indexed season), and a **63-team full clone** (684 repositories, with a parsed-call DuckDB > index). The same fact can therefore carry different denominators — 24/55 and 27/63 are both correct > measurements of IO-seam adoption, taken against two different bases. Every count in these chapters > names its denominator. ## Chapters **F. The control path and abstraction** 15. [The control path, end to end](15-control-path.md) — how a teleop or auto command becomes a motor voltage, and how state flows back up. 16. [Hardware abstraction and the IO line](16-hardware-abstraction.md) — what the IO seam really is, where the control loop sits, and how vendor types leak. 17. [Motor interfaces](17-motor-interfaces.md) — the device-level contract: the reusable `MotorIO` shapes the corpus actually uses. **G. The subsystems** 18. [Subsystem archetypes](18-subsystem-archetypes.md) — the IO quartet per control type, with the sim model each uses. 19. [The drivetrain subsystem](19-the-drivetrain-subsystem.md) — the swerve special case: modules, kinematics, odometry, and the CTRE-vs-owned-seam spectrum. **H. Perception and coordination** 20. [The world model](20-the-world-model.md) — `RobotState` and sensor fusion in depth. 21. [Vision systems](21-vision-systems.md) — the pipeline, what teams run, and what it produces. 22. [Coordination I — state machines and the superstructure](22-coordination-state-machines.md) — the FSM, the centralized manager, guarded interlocks. 23. [Coordination II — state graphs and behavior trees](23-coordination-graphs-trees.md) — the far end of the coordination ladder. # 15. The control path, end to end https://frc-code-scout.jointheleague.org/part-2/15-control-path/ *This is the deep dive Part I deferred and the map for everything that follows in Part II. A button press or an autonomous routine becomes a robot-wide goal, becomes per-subsystem setpoints, becomes an IO call, becomes a motor voltage — and measured state flows back up the same structure to a single world model. Trace that path once and the rest of Part II is detail.* Part I argued for the shape of the architecture: three seams ([the architecture in five views](../part-1/02-five-views.md)) built foundation-first in a fixed order ([the foundation-first appendix](../appendices/how-we-developed-this/05-foundation-first.md)). It did not show how data actually moves through that shape during a 20 ms cycle. That is this chapter. We trace one full control path — from a teleop binding or an auto routine all the way down to a motor voltage, and the measured state all the way back up — and then walk several concrete scenarios that are each a specialization of the same loop. The examples are abridged from the build spec; the point is the data flow, not a drop-in implementation. Treat this chapter as the orienting map for Part II. Each later chapter zooms in on one stop along the path traced here: [hardware abstraction at the IO line](16-hardware-abstraction.md), the [motor interfaces](17-motor-interfaces.md) below it, the [subsystem archetypes](18-subsystem-archetypes.md) that sit on it and [the drivetrain](19-the-drivetrain-subsystem.md), [the world model](20-the-world-model.md) the path reads from and [the vision systems](21-vision-systems.md) that feed it, and the coordination layer that turns intent into setpoints ([state machines and the superstructure](22-coordination-state-machines.md), [state graphs and behavior trees](23-coordination-graphs-trees.md)). --- ## 15.1 The layered overview Everything in the control path lives inside a small core: the IO seam, the state seam, and the coordination seam, plus a logging facade. Each advanced capability — vision fusion, pathfinding, replay, unit tests, a state graph — attaches to one of these seams as an addition, not a rewrite. ```d2 direction: down FOUND: "FOUNDATION — the core control path" { OI: "Driver / Operator bindings (RobotContainer)" SUP: "Superstructure coordinator (goal in, setpoints out, guarded transitions)" SUBS: "Subsystems (logic only)" { DR: Drive EL: Elevator AR: Arm MAN: Manipulator } RS: "RobotState (pose estimator + world model)" IOSEAM: "IO seam — one interface per subsystem" { IOI: "XxxIO interface + XxxIOInputs struct" REAL: "XxxIOReal (hardware)" SIM: "XxxIOSim (stub now, physics later)" } LOG: "Logging facade (AdvantageKit or DogLog — swappable)" } FOUND.OI -> FOUND.SUP FOUND.SUP -> FOUND.SUBS FOUND.SUBS -> FOUND.IOSEAM.IOI FOUND.IOSEAM.IOI -> FOUND.IOSEAM.REAL FOUND.IOSEAM.IOI -> FOUND.IOSEAM.SIM FOUND.SUBS -> FOUND.RS FOUND.RS -> FOUND.SUP FOUND.SUBS -> FOUND.LOG: "publish Inputs" { style.stroke-dash: 3 } FOUND.RS -> FOUND.LOG: "publish pose" { style.stroke-dash: 3 } ADD: "ADD-ONS — attach to seams later" { VIS: "Vision (VisionIO) → RobotState" PP: "PathPlanner / Choreo autos" PF: "On-the-fly pathfinding" REPLAY: "Log replay (AdvantageKit)" TESTS: "Unit tests vs XxxIOSim" GRAPH: "State-graph / motion planner" } ADD.VIS -> FOUND.RS: "writes vision measurements" { style.stroke-dash: 3 } ADD.PP -> FOUND.SUP: "requests Superstructure goals" { style.stroke-dash: 3 } ADD.PF -> FOUND.RS: "consumes RobotState pose" { style.stroke-dash: 3 } ADD.REPLAY -> FOUND.IOSEAM.IOI: "feeds logged Inputs" { style.stroke-dash: 3 } ADD.TESTS -> FOUND.IOSEAM.SIM: "construct subsystem w/ Sim" { style.stroke-dash: 3 } ADD.GRAPH -> FOUND.SUP: "replaces transition fn" { style.stroke-dash: 3 } ``` Read the solid arrows top to bottom and that is the command path: bindings call the Superstructure, the Superstructure sets goals on subsystems, subsystems call their IO, IO drives hardware. Read the arrows that feed `RobotState` and the logging facade and that is the data path coming back up. The dotted add-ons all hang off one of the three seams; none of them reach into a subsystem to do their job. The signature of the foundation, on disk, is a four-file quartet per mechanism — `XxxIO`, `XxxIOInputs`, an `XxxIO`, and `XxxIOSim`. If a new team member can find those four files for any mechanism, the architecture is intact. (Naming note from the corpus: the hardware impl is named by device — `ElevatorIOTalonFX`, `GyroIOPigeon2`, `VisionIOLimelight` — not literally `IOReal`. [Chapter 16](16-hardware-abstraction.md) covers that line in depth.) --- ## 15.2 Run mode: REAL, SIM, REPLAY, and the single selection point The same code runs in three modes. The only thing that changes between them is which IO implementation is constructed, and that choice happens in exactly one place — the robot constructor, keyed off the run mode. ```d2 direction: right START: "Robot constructor" MODE: "Run mode?" { shape: diamond } R: "new ElevatorIOReal() new VisionIOPhoton()" S: "new ElevatorIOSim() new VisionIOSim()" Z: "new ElevatorIO() {} (no-op; inputs come from log)" BUILD: "construct subsystems" START -> MODE MODE -> R: REAL MODE -> S: SIM MODE -> Z: REPLAY R -> BUILD S -> BUILD Z -> BUILD ``` Three things follow from this one selection point. - **REAL** constructs hardware implementations. `updateInputs` reads real sensors; `setVoltage`/`setSetpoint` commands real motors. - **SIM** constructs simulation implementations. The same subsystem logic runs against a physics model instead of hardware; nothing above the IO line knows the difference. - **REPLAY** constructs an empty no-op implementation — `new ElevatorIO() {}` — that does nothing and reads nothing. In replay the inputs struct is overwritten from a recorded log before the subsystem ever sees it, so the no-op has nothing to do. You write this empty body on day one. It costs nothing and it is the entire reason replay later requires zero new subsystem code. Because the rest of the codebase only ever holds an `XxxIO` reference, the subsystems, commands, and Superstructure are written once and run unmodified in all three modes. The fork is one `switch`, not a parallel code path. That single selection point is what makes scenario §15.6.D (replay) free later in this chapter. --- ## 15.3 The 20 ms runtime loop: read → log → decide → actuate This is the heartbeat. Every scenario later in the chapter is a specialization of it. The `CommandScheduler` runs each subsystem's `periodic()` every cycle, and within a cycle the order is fixed. ```d2 shape: sequence_diagram Sched: CommandScheduler Sub: Subsystem IO: XxxIO impl HW: Hardware / Sim Log: Logging facade RS: RobotState Sup: Superstructure Sched -> Sub: periodic() Sub -> IO: updateInputs(inputs) IO -> HW: read sensors HW -> IO: raw values IO -> Sub: inputs filled Sub -> Log: publish Xxx/Inputs Sub."in REPLAY, log OVERWRITES inputs here" Sub -> RS: contribute odometry / mechanism state Sched -> Sup: (command) requested goal Sup -> RS: read pose / state Sup -> Sub: setGoal(...) → setpoint Sub -> IO: setVoltage / setSetpoint IO -> HW: actuate ``` The invariant is **read → log → decide → actuate**, in that order, every cycle. 1. **Read.** `updateInputs(inputs)` fills a mutable `XxxIOInputs` struct with everything coming back from the device — position, velocity, applied volts, current, temperature. One call, one struct, all the hardware readings in one place. 2. **Log.** The filled struct is published immediately, before any decision uses it. Logging right after reading is what makes the log a faithful record of exactly what the code saw — which is the precondition for deterministic replay. In REPLAY mode this is also the moment the recorded log overwrites the struct, so downstream code runs against the historical inputs. 3. **Decide.** Subsystems contribute their state to `RobotState` (drive contributes odometry); the Superstructure reads pose and state and turns the active goal into per-subsystem setpoints. 4. **Actuate.** Each subsystem pushes its setpoint down to its IO via `setVoltage` or `setSetpoint`, and the IO commands hardware. The struct at the center of step 1 is the artifact both logging stacks consume: ```java class XxxIOInputs { double positionMeters; double velocityMps; double appliedVolts; double currentAmps; double tempC; } ``` Building the inputs-struct style (the interface fills this object each cycle via `updateInputs`) rather than plain getters is the one decision that keeps logging and replay available later. It is a few lines of boilerplate per subsystem — annotate the struct `@AutoLog` for AdvantageKit, or hand-log its fields for DogLog — and it is why the log/replay choice can be deferred. [Chapter 17](17-motor-interfaces.md) goes further into what the IO contract carries beyond `updateInputs`/`setVoltage` (brake mode, current limits, characterization). ### Where the loop lives: above or below the line A second per-subsystem decision shapes the actuate step: the PID and feedforward can sit above the IO interface (the interface takes `setVoltage(volts)` and the subsystem owns one controller that Sim and Real share) or below it (the interface takes `setSetpoint(inches)` and each implementation runs its own loop, typically on-motor firmware). Keep the loop above the line for anything you simulate; push it below only to deliberately exploit firmware control such as Phoenix 6 MotionMagic — [chapter 16](16-hardware-abstraction.md) gives the full side-by-side comparison, and [chapter 18](18-subsystem-archetypes.md) maps which archetypes lean which way. --- ## 15.4 The state seam the path reads from The decide step above reads pose from `RobotState`, a single object that owns the robot's best estimate of the world behind a pose estimator. Subsystems feed it odometry; later, vision feeds it corrections; decisions and pathing read from it. ```java public class RobotState { private final SwerveDrivePoseEstimator estimator; // foundation // later: TimeInterpolatableBuffer history; // world-model seam public void addOdometry(SwerveModulePosition[] p, Rotation2d g, double t) {...} public void addVisionMeasurement(Pose2d p, double t, Matrix stdDevs) {...} public Pose2d getPose() {...} } ``` On day one only `addOdometry` and `getPose` are exercised. `addVisionMeasurement` exists but is uncalled — it is the vision seam, pre-cut. Centralizing the estimate here, rather than letting the drive subsystem privately own it, is what lets vision, pathfinding, and auto all share one consistent world model. [Chapters 20](20-the-world-model.md) [and 21](21-vision-systems.md) develop this seam in full. ## 15.5 The coordination seam the path passes through The decide step's "turn a goal into setpoints" happens in the `Superstructure`. It takes one robot-wide goal and fans it out to subsystems through a single transition function that may reject or reorder a transition for safety. ```java public class Superstructure extends SubsystemBase { // SubsystemBase: schedulable, owns run/runOnce public enum Goal { STOW, INTAKE, SCORE_L4, CLIMB } public Command requestGoal(Goal g) { return applyGoal(g); } private Command applyGoal(Goal g) { // <-- the seam. interlocks live here. return switch (g) { case SCORE_L4 -> Commands.sequence( // sequenced, each step guarded — arm.setGoalCommand(CLEAR), // three plain setGoal() calls would Commands.waitUntil(arm::atGoal), // all land in the same cycle and elevator.setGoalCommand(L4), // the last would overwrite the first Commands.waitUntil(elevator::atGoal), arm.setGoalCommand(SCORE)); // ... }; } } ``` (`CLEAR`, `L4`, `SCORE` — like `setModuleStates` in the scenarios below — are schematic names, not a real API.) The seam is that **all transitions pass through one function**. The foundation version is a `switch` over a goal enum; later its body can be replaced with a guarded transition table, a motion planner, or a graph search without changing a single caller. The two coordination chapters cover that progression: [chapter 22](22-coordination-state-machines.md) for the state-machine form, [chapter 23](23-coordination-graphs-trees.md) for graphs and trees. --- ## 15.6 Scenarios — walking the data flow The loop above is the general case. Here are concrete control paths through it. ### 15.6.A Vision → pose → path: how the robot makes a path from what it sees *Goal: see the field, decide where to score, drive there.* This flow touches all three seams in sequence: **VisionIO → RobotState → Superstructure → pathfinder → DriveIO.** ```d2 shape: sequence_diagram Cam: "Coprocessor (PhotonVision/Limelight)" VIO: VisionIO impl RS: RobotState Sup: Superstructure PF: "Pathfinder (PathPlanner/Choreo)" Drive: Drive subsystem DIO: DriveIO Cam -> VIO: AprilTag observation (pose + tags + latency) VIO -> RS: addVisionMeasurement(pose, t, stdDevs) RS."fuse with odometry (reject if stdDev/ambiguity high)" Sup -> RS: getPose() (where am I?) Sup -> Sup: pick target = nearest legal scoring pose Sup -> PF: generate path(currentPose → targetPose) PF -> Drive: trajectory / chassis speeds "until at target": { Drive -> RS: getPose() (closed-loop on fused pose) Drive -> DIO: setModuleStates(...) } Drive -> Sup: at target Sup -> Sup: applyGoal(SCORE_L4) ``` The camera writes a measurement to `RobotState`; everything downstream reads pose from the one fused estimate. **Vision never talks to drive.** Swapping PhotonVision for Limelight is a new `VisionIO` impl. Swapping PathPlanner for Choreo, or adding on-the-fly pathfinding, changes only the pathfinder participant. The seams localize every change. This is the same `getPose()` from §15.4 and the same `applyGoal` from §15.5 — the scenario just chains them. ### 15.6.B The interlock: refusing an illegal physical state *Constraint: the manipulator/scoop must not be open while the elevator is raised (they collide).* This lives in exactly one place — the Superstructure's guarded transition — never scattered across subsystems. ```d2 direction: down REQ: "requestGoal(SCORE_L4)" GUARD: "Guard: is scoop open AND elevator low?" { shape: diamond } RAISE: "elevator.setGoal(L4)" SEQ: "sequence the safe order" S1: "1. manipulator.setGoal(CLOSED)" S2: "2. waitUntil(manipulator.isClosed)" S3: "3. elevator.setGoal(L4)" S4: "4. manipulator.setGoal(SCORE)" DONE: "goal active" REQ -> GUARD GUARD -> RAISE: safe to raise GUARD -> SEQ: unsafe SEQ -> S1 S1 -> S2 S2 -> S3 S3 -> S4 RAISE -> DONE S4 -> DONE ``` In the foundation, `applyGoal` hand-sequences the safe order with `Commands.sequence(...)` and `waitUntil(...)`, where each subsystem exposes a cheap predicate (`isClosed()`, `isStowed()`) read from its inputs struct. Later, a declarative transition table or a motion planner that knows mechanism geometry can compute the safe interpolation automatically — and because every transition already routes through `applyGoal`, you replace the body, not the callers. The subsystems stay dumb: they execute setpoints and report state. The knowledge that two states are mutually exclusive lives in the coordinator, the only object that sees all mechanisms at once. [Chapter 22](22-coordination-state-machines.md) develops the guard from a `switch` into a real transition function. ### 15.6.C Operator intent vs execution *An operator presses "score L4."* This shows why intent is separated from execution. ```d2 shape: sequence_diagram Op: Operator RC: RobotContainer Sup: Superstructure El: Elevator Man: Manipulator Op -> RC: button L4 RC -> Sup: requestGoal(SCORE_L4) Sup -> Sup: applyGoal — runs interlock (§15.6.B) Sup -> El: setGoal(L4) Sup -> Man: setGoal(SCORE) (after guard clears) Man."each subsystem closes its own loop to its setpoint via its IO" ``` The operator expresses *intent* (a goal). The Superstructure owns *execution* (the legal, sequenced setpoints). The operator binding never commands a motor, so re-tuning the scoring sequence is a one-place change — and the same `SCORE_L4` goal is reusable as an autonomous action (it is the final `applyGoal(SCORE_L4)` step in scenario 15.6.A). One goal, two callers, identical execution. ### 15.6.D Replay a match to find the bug *The robot mis-scored in qualifier 42; reproduce it at your desk.* This scenario requires zero code written for the purpose. It is the dividend of the inputs-struct IO seam (§15.3) plus the REPLAY run mode (§15.2). ```d2 shape: sequence_diagram Log: Match WPILOG Robot: Your code (unchanged) IO: XxxIO (REPLAY no-op) AS: AdvantageScope Log -> Robot: launch in REPLAY mode, feed log "every logged cycle": { Log -> IO: overwrite Inputs with logged values Robot -> Robot: run real decision code on logged inputs Robot -> AS: emit NEW computed outputs alongside originals } Robot."step through, add fields, see exactly what the superstructure decided and why" ``` Recall the loop's order: read → log → decide → actuate. Replay intercepts at the log step. Instead of reading hardware, the recorded log overwrites the inputs struct, and then the actual decision code runs against the actual sensor inputs from the match, deterministically. You can add new logged fields to inspect decisions that were not logged live — see exactly what the Superstructure decided and why. The only prerequisites are that the foundation logged the inputs struct and wired a REPLAY mode, both of which §15.2 and §15.3 already did. Nothing in the subsystems changes; the no-op IO simply has nothing to do because the inputs arrive from the log. This is why teams that build the seam and never collect replay are leaving the dividend on the table. The afternoon of work that fills `XxxIOSim` for simulation also unlocks unit tests (point a test at the sim) and replay (flip the mode). The foundation already paid for all three. --- ## 15.7 The path, in one breath A binding or auto routine requests a `Goal`. The Superstructure's `applyGoal` reads `RobotState`, runs any interlock, and sets per-subsystem setpoints. Each subsystem, every 20 ms, reads its IO inputs, logs them, decides against the active setpoint, and actuates through its IO down to a motor voltage. Measured state flows back up: IO inputs into the subsystem, odometry and corrections into `RobotState`, pose back into the Superstructure for the next decision. Vision feeds the state seam without touching drive; replay feeds the IO seam without touching subsystems; coordination changes the Superstructure body without touching callers. Everything in Part II is a closer look at one stop on that path. The next chapter starts at the bottom — the IO line itself, and what it means to keep vendor types below it: [chapter 16, hardware abstraction and the IO line](16-hardware-abstraction.md). # 16. Hardware abstraction and the IO line https://frc-code-scout.jointheleague.org/part-2/16-hardware-abstraction/ *Part I argued that a subsystem should depend on a contract, not on a motor. This chapter shows the mechanics: the IO layer is the Strategy pattern, "IO layer" and "hardware abstraction" name two different axes, and a single diagnostic — which side of the line the control loop is on — classifies any interface you find.* Part I, [chapter 3](../part-1/03-the-io-seam.md), motivated the IO seam and then deferred the implementation. This is that deferred deep dive. The case for the seam is made; here we open the files and show how the boundary is actually built, what every implementation in the corpus shares, and the choices that distinguish one team's IO layer from another's. The IO seam appears in some form in 24 of the 55 season-index repos — the single most widely shared architectural idea in serious FRC code, and the default rather than the exception among the strong programs. That convergence is the subject of the rest of this chapter: not whether to draw the line, but where it sits and what crosses it. ## The pattern has an older name "IO layer" is FRC's house name. The pattern is Strategy. Strategy, in the Gang of Four sense, defines a family of interchangeable implementations behind one interface and lets the caller select which one is in force — and lets that implementation vary independently of both the interface and the code that uses it. That is an IO layer exactly. `ElevatorIO` is the strategy interface; `RealElevator`, `SimElevator`, and `NoElevator` are interchangeable strategies; the subsystem holds a reference to one without knowing which. The reason to reach for Strategy is that the implementation can change over time without any change to the interface or to the code that depends on it. A team writes the subsystem's logic once against `ElevatorIO`, then swaps the strategy underneath: real hardware during a match, a physics model on a laptop, a log-replay stub after the match, a different motor vendor next season. None of those swaps touches the interface, and none touches the subsystem. Teach it as Strategy first and "IO layer" second — the FRC name describes where the pattern sits, the GoF name describes what it is. Two neighboring patterns are worth naming so they stay distinct: - **Factory Method** is the selection step. SciBorgs' `Elevator.create()` and `Elevator.none()` are factory methods that choose which strategy to instantiate. The factory picks the strategy; the strategy does the work. - **Null Object** is the do-nothing strategy. `NoElevator` is a full member of the strategy family whose methods are deliberately empty, so a subsystem with disconnected hardware runs as a safe no-op instead of crashing. It is a strategy, not an absence of one. ### Strategy vs. Bridge A true hardware abstraction layer — "any motor accepts a voltage, regardless of vendor" — is structurally similar to the IO layer but is better described as **Bridge**. Bridge splits an abstraction (motor) from its implementation (TalonFX driver) so the two evolve separately: a design-time structural concern. Strategy and Bridge look almost identical in code. GoF distinguishes them by intent: | | Strategy | Bridge | |---|---|---| | What it swaps | Behavior, at runtime | Implementation, decided once | | When the choice happens | Every deploy (real vs. sim vs. replay) | At design time (device independence) | | The IO layer | Leans this way | Less so | The IO layer leans Strategy because the swap is a runtime choice made on every deploy. The distinction matters later in this chapter, in "The line that matters," where the line between the two blurs in real code. ## The two axes that get tangled There is a persistent ambiguity in FRC about whether the IO layer *is* a hardware abstraction layer. Resolving it changes how you read every interface in the corpus. **"IO layer" names a location. "Hardware abstraction" names a property.** - The IO layer is *where* the boundary sits — one interface per subsystem, at the line between the subsystem's logic and its devices. - Hardware abstraction is a *property* an interface may or may not have — independence from the specific device. A given interface can sit at the IO-layer location while having very little true hardware abstraction, or a lot. They are not the same axis, which is why they feel tangled. The next sections take them apart with a single diagnostic. ## What every implementation shares Strip away the per-team decoration and every IO layer in the corpus has the same four parts. **1. An interface that names the hardware boundary.** One interface per subsystem (`ElevatorIO`, `GyroIO`, `ModuleIO`), declaring only the operations that cross the hardware line: a way to read the device's current state and a way to command outputs to it. No control logic, no game logic — just the contract. This is dependency inversion: the subsystem depends on an abstraction, not on a `TalonFX`. **2. Output methods — commands going to hardware.** Methods that actuate the device. At the subsystem level these are commands like `setPosition(double inches)`; at the raw device level they are primitives like `runVolts(double)`. Which of these an interface exposes is not cosmetic — it tells you where the control loop lives, the subject of the next section. **3. An inputs channel — state coming back from hardware.** Every implementation reports position, velocity, applied voltage, current, temperature, and a connected flag back up to the logic. This is the one place the two dominant styles truly diverge (see Variation 1): some teams pass a mutable inputs object the implementation fills in; others expose plain getters. Both answer "what is the hardware doing right now?" **4. Interchangeable implementations, selected at construction.** At minimum a real one (talks to motors) and a sim one (talks to a WPILib physics model). The subsystem is handed one in its constructor and never learns which. Selection happens in exactly one place — a constructor call, a `create()` factory, or a switch on robot identity — so the entire behavior of "run on a real robot" versus "run on a laptop" turns on a single line. 6328's drivetrain shows the selection in its barest form — the same `Drive` object built three ways depending on what hardware is present: ```java // 6328 — RobotContainer.java, implementation selection (abridged) switch (Constants.getRobot()) { case COMPBOT -> // real competition robot drive = new Drive(new GyroIOPigeon2(), new ModuleIOComp(0), new ModuleIOComp(1), /* ... */); case SIMBOT -> // laptop simulation drive = new Drive(new GyroIO() {}, // anonymous no-op gyro new ModuleIOSim(), new ModuleIOSim(), /* ... */); default -> // replay: feed everything from logs drive = new Drive(new GyroIO() {}, new ModuleIO() {}, new ModuleIO() {}, /* ... */); } ``` That `new GyroIO() {}` — an empty inline implementation — is worth noticing. When every interface method has a default empty body, the no-op implementation costs zero extra files. It is the replay/absent case for free. ## The line that matters: where the control loop lives The control loop is the code that turns "go to height H" into the moment-by-moment voltages that get there — a PID plus a feedforward. Where that loop sits is the diagnostic that classifies any IO interface. One question cuts cleanly: **which side of the interface is the control loop on?** ### Loop above the line — the interface is a device pipe If the subsystem holds the PID and feedforward and computes a voltage every cycle, the only thing left to send across the interface is that voltage. The interface ends up exposing `setVoltage(double)` — a raw actuator primitive. SciBorgs do this: their `Elevator` subsystem owns a `ProfiledPIDController` and an `ElevatorFeedforward` as fields, computes the output, and pushes volts down through the IO. ```java // 1155 SciBorgs — the loop sits in the subsystem, so the IO takes volts // in Elevator.java (the SUBSYSTEM, above the interface): private final ProfiledPIDController pid = new ProfiledPIDController(kP, kI, kD, /* ... */); private final ElevatorFeedforward ff = new ElevatorFeedforward(kS, kG, kV, kA); // interface (below): a dumb pipe to the motor public interface ElevatorIO extends AutoCloseable { void setVoltage(double voltage); // raw actuator command double position(); // meters double velocity(); // meters/sec void resetPosition(); } ``` An `ElevatorIO` with a `setVoltage` method reads at the wrong level for an elevator — and that instinct is the correct diagnostic. "Set voltage" is a motor verb, not an elevator verb. Its presence is the tell that the elevator's intelligence lives *above* this interface, and the interface is functioning as a hardware abstraction layer — a thin device pipe — with the subsystem as the smart layer on top. One impurity is worth noting: SciBorgs' `position()` returns meters, an elevator unit, so the gear-ratio conversion has leaked below the line. A perfectly pure device pipe would return motor rotations and convert above. Real code is rarely that pure. ### Loop below the line — the interface is a subsystem-intent contract If instead each implementation carries its own controller, the interface can speak in the subsystem's own terms — `setPosition(inches)` — and the implementations are responsible for running the loop that gets there. PhantomCatz do this. Their interface commands a position *and* exposes the gains, because the controller lives in the implementation (here, CTRE's on-motor MotionMagic firmware): ```java // 2637 PhantomCatz — gains pushed BELOW the line; the loop runs in the implementation default void setPosition(double inches) {} // intent default void setGainsSlot0(double kP, double kI, double kD, double kS, double kV, double kA, double kG) {} // the loop's gains... default void setMotionMagicParameters(double cruiseVelocity, // ...live below double acceleration, double jerk) {} ``` Here the interface reads at the right level for an elevator, and the implementations are full hardware bundles, each owning the control math. This is the "fat IO" or true subsystem-intent style. Its cost is that the loop is reimplemented (or reconfigured) per strategy — the real one tunes firmware MotionMagic, the sim one needs its own controller to honor `setPosition`. The full PhantomCatz interface is the same one quoted in Part I: the output verb is `setPosition`, the readback is `positionInch`, and the elevator never traffics in volts at the subsystem level. ```java // 2637 PhantomCatz — CatzElevator/ElevatorIO.java (abridged to the essential surface) public interface ElevatorIO { @AutoLog class ElevatorIOInputs { public double positionInch = 0.0; // read: height public double velocityInchPerSec = 0.0; // read: speed public boolean isLeaderMotorConnected = false; } default void updateInputs(ElevatorIOInputs inputs) {} default void setPosition(double inches) {} // command: go to a height default void setBrakeMode(boolean enabled) {} default void stop() {} } ``` ### The rule Both styles are legitimate Strategy implementations sitting at the IO-layer location. They differ only on loop placement, and that placement is what makes one interface look like a HAL and the other like a subsystem contract. | | Loop ABOVE the line | Loop BELOW the line | |---|---|---| | Interface command | `setVoltage(volts)` | `setPosition(inches)` | | What it reads as | A device pipe (HAL-like) | A subsystem-intent contract | | Where PID/FF lives | In the subsystem, written once | In each implementation | | Implementations are | Trivial (forward the volts) | Full bundles (each runs a loop) | | Corpus example | SciBorgs 1155 | PhantomCatz 2637 (CTRE MotionMagic) | So: a pure IO layer is Strategy applied at subsystem granularity, and whether it *also* looks like a hardware abstraction layer depends entirely on whether you left the control loop above it. See `setVoltage` on something named for a mechanism — the loop is above, the interface is a HAL. See `setPosition` — the loop is below, the interface is a subsystem contract. Same location, same pattern, different placement of the brains. The detailed control-path treatment is in [chapter 15](15-control-path.md). ## Selecting the strategy Selection lives in one place. PhantomCatz switch on robot mode, with an anonymous no-op for replay: ```java // 2637 PhantomCatz — strategy selection elevatorIO = switch (Constants.getRobotMode()) { case REAL -> new ElevatorIOReal(); // CTRE MotionMagic to a position case SIM -> new ElevatorIOSim(); // physics model to a position case REPLAY -> new ElevatorIO() {}; // no-op: logs drive the inputs }; ``` SciBorgs do the same selection as factory methods, with an explicit null-object strategy for the disabled-hardware case: ```java // 1155 SciBorgs — Elevator.java, factory selection + null object public static Elevator create() { return new Elevator(Robot.isReal() ? new RealElevator() : new SimElevator()); } public static Elevator none() { return new Elevator(new NoElevator()); // Null Object: a do-nothing strategy } ``` One interface, interchangeable strategies, selected in one place, swappable over time without touching the interface or the subsystem. ## Four variations Teams differ on four axes. None is right or wrong; each is a trade between boilerplate, logging power, and language ergonomics. ### Variation 1 — Inputs struct vs. direct getters (the logging fork) The deepest fork, and it traces directly to a logging decision. **The getter style (SciBorgs).** The interface exposes `position()` and `velocity()` as plain methods. Simple, readable, and the logic just calls them. The cost: nothing about the hardware state is automatically logged — you log what you choose to, separately. **The inputs-struct style (6328 / AdvantageKit).** The interface has no read methods at all. Instead it has one method, `updateInputs(inputs)`, that fills a mutable data object. That object is annotated `@AutoLog`, and AdvantageKit serializes every field to the match log every cycle. Here is 6328's gyro — exactly one method, and the "read" half of the interface is the struct it populates: ```java // 6328 — GyroIO.java (complete, header trimmed) public interface GyroIO { @AutoLog class GyroIOInputs { public GyroIOData data = new GyroIOData(false, Rotation2d.kZero, 0, /* ... */); public double[] odometryYawTimestamps = new double[] {}; public Rotation2d[] odometryYawPositions = new Rotation2d[] {}; } record GyroIOData(boolean connected, Rotation2d yawPosition, double yawVelocityRadPerSec, Rotation2d pitchPosition, /* ... */) {} default void updateInputs(GyroIOInputs inputs) {} // the only method } ``` The struct style is what makes whole-match log replay possible. Because every value crossing the hardware boundary lands in a logged struct, AdvantageKit can later feed a recorded log back through the real code and reproduce exactly what the robot decided. The getter style cannot do this — it has no single chokepoint to record. That replay guarantee is also why 6328's entire robot program must be single-threaded and deterministic: an architectural invariant dictating a coding rule. The trade: the struct style is more boilerplate (a data class plus an inputs wrapper per subsystem) bought in exchange for free, complete, replayable telemetry. The getter style is less code and is fine until you want to debug a match you can no longer reproduce. ### Variation 2 — Per-subsystem interface vs. one generic base 6328 writes a fresh `XxxIO` interface for every mechanism. 254 noticed that most position-controlled mechanisms (elevator, arm, wrist, pivot) need the same handful of motor operations, and collapsed them into a single parameterized base class: ```java // 254 — the generic servo subsystem (signature) class ServoMotorSubsystem< T extends MotorInputsAutoLogged, U extends MotorIO> extends SubsystemBase { /* ... */ } ``` A concrete mechanism becomes a thin subclass plus a config object holding gains, gear ratios, and limits. There is one `MotorIO` interface and one `TalonFXIO` / `SimTalonFXIO` pair shared across every mechanism. The trade: maximum reuse and almost no per-mechanism code, paid for with heavy generics and a steeper on-ramp for a new student. 6328's per-subsystem interfaces are more verbose but each one is independently readable. The archetype split that makes the generic base possible is the subject of [chapter 18](18-subsystem-archetypes.md). ### Variation 3 — The null object: named class vs. anonymous Every mature IO layer has a third implementation beyond real and sim: the do-nothing one, for running with a mechanism unplugged or replaying from logs. Two ways to express it. - **Named class** (SciBorgs `NoElevator`, PhantomCatz `ElevatorIONull`). Explicit, greppable, self-documenting — there is a file you can point at. The null-object pattern made visible. - **Anonymous inline** (`new GyroIO() {}`, 6328). Costs zero files because the interface's methods all have empty default bodies, so an empty implementation is automatically a safe no-op. 6328 uses this 30+ times across the codebase for replay and sim-stub cases. The anonymous form only works with the struct style, where methods can default to empty. The getter style returns values, so its no-op must return something (`return 0;`) — which is why SciBorgs writes `NoElevator` out as a real class. The two forks are not independent: the logging choice (Variation 1) constrains the null-object choice (Variation 3). ### Variation 4 — The language carries part of the pattern In Kotlin (3636, 4099) the architecture is identical to 6328's, but the language enforces for free what Java teams hand-roll. The same `FunnelIO` interface, a real implementation, and a sim implementation — but the singleton, the unit types, and the inputs class are language features, not boilerplate: ```kotlin // 3636 — FunnelIO.kt (abridged; active fields shown, commented telemetry omitted) @Logged open class FunnelInputs { /* logged fields */ } interface FunnelIO { fun setSpeed(percent: Double) fun setVoltage(voltage: Voltage) // Voltage is a *type*, not a double fun updateInputs(inputs: FunnelInputs) } class FunnelIOReal : FunnelIO { private var rampMotor = TalonFX(CTREDeviceId.FunnelMotor).apply { /* ... */ } override fun setVoltage(voltage: Voltage) { assert(voltage.inVolts() in -12.0..12.0) rampMotor.setVoltage(voltage.inVolts()) } override fun updateInputs(inputs: FunnelInputs) { /* ... */ } } class FunnelIOSim : FunnelIO { private var simMotor = FlywheelSim(system, motor, 0.0) override fun setSpeed(percent: Double) { simMotor.inputVoltage = percent * 12 } override fun updateInputs(inputs: FunnelInputs) { simMotor.update(Robot.period) } } ``` The `Voltage` parameter type is the lesson: in Kotlin a height and a voltage are different types, so handing a motor a distance where it wants a voltage is a compile error rather than a runtime mystery. Putting the same elevator IO in Java and Kotlin side by side makes visible which lines are the design and which are merely Java ceremony. ## Naming the implementations The corpus names implementations by device, not by the abstract word "Real": - The sim impl is reliably `XxxIOSim` (or `SimXxx`). - The hardware impl is named **by device** — `XxxIOTalonFX`, `XxxIOSparkMax`, `XxxIOKrakenX60`, `GyroIOPigeon2`, `VisionIOLimelight` — **not `XxxIOReal`**. Only ~5 of the 55 season repos use `Real`. Naming the device documents the vendor at the seam. - The `XxxIOInputs` struct always co-exists with the interface; the null-object / replay variants are real but rare (~1 team ships a dedicated replay impl). The four files that constitute a subsystem (the "quartet") are: ``` xxx/ XxxIO.java // the contract: what the subsystem needs from hardware XxxIO.java // the hardware impl — the ONLY file that imports a vendor SDK XxxIOSim.java // the simulation impl — wraps a WPILib sim model; WPILib only Xxx.java // the subsystem — holds ONE XxxIO, owns control logic, exposes Commands ``` The motor-interface layer that sits underneath these — `MotorIO`, `TalonFXIO`, and friends — is the subject of [chapter 17](17-motor-interfaces.md). ## Vendor confinement and leak detection The IO line is only worth drawing if vendor types stay below it. The rule: > A `TalonFX`, a `PhotonCamera`, a `SparkMax` appears **only** inside a `XxxIO` or `XxxIOSim` file — never in the subsystem, a command, or the `Superstructure`. The moment `com.ctre` / `com.revrobotics` / `org.photonvision` appears above the line, a tool swap becomes a refactor and the subsystem stops being portable. This is the most-violated rule in the corpus: **22 of 24** IO-layer teams leak a vendor type above the line at least once. Clean confinement is a distinguishing marker, not a baseline. Enforce it mechanically with a checkstyle/spotless import rule rather than by review alone. ### How to detect a leak The detection is a directed import search. For each subsystem package, grep the files *above* the IO line (`Xxx.java`, commands, `Superstructure`, `RobotState`) for the three vendor roots: ``` com.ctre com.revrobotics org.photonvision ``` Any match above the line is a finding. The agent-facing review workflow in the code-review principles makes this step 2 of a deterministic pass: 1. Map touched seams (IO / state / coordination). 2. **Check vendor confinement** — search changed files for vendor imports above IO implementations. 3. Check interface shape — does the contract still support REAL + SIM without vendor leakage? 4. Check dependency direction — no upward dependency from subsystem to coordinator/state. 5. Check test path — can the code still be instantiated in sim-backed tests? 6. Check sim path — no SIM-only behavior fork. 7. Check logging path — inputs and key decisions remain observable. 8. Classify findings by severity. Steps 2 and 3 are the two red flags specific to the IO line: - A subsystem imports `com.ctre`, `com.revrobotics`, or `org.photonvision` directly. - An interface exposes vendor-specific handles or configuration objects, or mirrors the vendor API surface without subsystem intent. The evidence of correctness is symmetric: vendor imports exist *only* in `XxxIO` and `XxxIOSim`, the subsystem constructor accepts an interface (`XxxIO`) rather than a vendor object, and the interface can be implemented by both a hardware class and a physics-sim class. ### Severity The code-review principles assign severity by the invariant a change breaks, not by whether the robot currently runs: | Severity | Meaning | Applied to the IO line | |---|---|---| | S0 Blocker | Breaks an architectural invariant or makes safe operation unverifiable | A subsystem that cannot execute with `XxxIOSim` at all; new mechanism code skipping the IO seam by design | | S1 High | Preserves runtime behavior today but creates a near-term test/sim/replay dead end | A vendor type leaked above the line; an interface that a sim class cannot implement | | S2 Medium | Local design smell that increases coupling or hides intent | An interface method that mirrors a vendor call without subsystem intent | | S3 Low | Readability or consistency issue | Naming the hardware impl `XxxIOReal` instead of by device | Two framings from the principles govern how a reviewer treats these. First: **review what is used, not what is present** — a vendordep in the build file is not adoption, and an `XxxIO` interface that no implementation actually swaps is not a seam. Confirm by opening the files. Second: **treat architecture regressions as functional regressions** — a leaked vendor type is a finding even when the robot behaves correctly on the field. "Works on robot" is not a justification for bypassing the abstraction, and "will refactor later" for a vendor leak is architectural debt that must carry a dated remediation plan. The non-negotiable anti-patterns that auto-fail a review, restricted to the IO line, are: vendor types imported above IO implementations; subsystem logic that cannot execute with `XxxIOSim`; and new mechanism code skipping the IO seam by design. ## Why confinement pays The three rules below are the same property seen three ways — you can test in isolation and lift the subsystem out as a library *because* the vendor and the siblings are kept out: 1. **Mock below, test above.** Because the sim impl is just another `XxxIO`, you can construct the *real* subsystem against *fake* hardware and unit-test it with zero hardware and zero other subsystems — `new Elevator(new SimElevator())`, command a setpoint, step the sim, assert it arrived. 2. **Rip it out as a library.** The subsystem package imports WPILib plus its own contract and no sibling subsystem. If a sibling import sneaks in, the seam has leaked. 3. **Never import a vendor type above the IO line.** The rule of this chapter, restated as an ethic. These are the IO layer's deferred dividends — simulation, unit testing, and replay all fall out of the one interface. They are the subject of the cross-cutting-practices chapters; the seam built here is what makes them mechanically possible. --- Next: [chapter 17 — motor interfaces](17-motor-interfaces.md), the layer beneath the subsystem IO where `MotorIO` and its device implementations live. # 17. Motor interfaces https://frc-code-scout.jointheleague.org/part-2/17-motor-interfaces/ *Part I drew the IO seam at the subsystem: one `ElevatorIO` per mechanism, vendor types sealed inside the implementation. This chapter goes one level lower, to the device. It surveys how teams in the corpus actually talk to motors in code — the reusable `MotorIO` contracts six teams build, the design axes they disagree on, and the rarer idea of capability-typed devices where an interface is named for what it does, not the brand that does it. It is the prior art on which Part III will build a single portable motor interface.* This is a deep dive supporting [Part I ch. 3, the IO seam](../part-1/03-the-io-seam.md). That chapter established the rule: a subsystem speaks mechanism semantics (`setHeight(m)`), and the vendor type (`TalonFX`, `SparkMax`) lives only inside the IO implementation. Here we look at what happens *below* that line — at the device-level abstractions a handful of teams build to talk to motors directly. Two questions drive the chapter. First, the corpus survey: how do FRC teams interact with motors in code, and what does the abstraction look like when they build a reusable one? Second, the alternative: the capability-typed-devices idea, which appeared in [Part I's alternatives entry](../part-1/08-alternatives.md) and is shown here in full — interfaces named by capability (`PositionMotor`, not `ITalonMotor`), a single hardware object that constructs and configures every device, and FOC modeled as an orthogonal opt-in. Part III will propose a single portable motor interface distilled from this evidence; see [Part III](../part-3/) generally. This chapter is the history that proposal answers to. ## The landscape underneath Motor hardware in the corpus is near-universal CTRE plus REV, and most teams run both vendors at once. | Vendor type | Teams (of 63) | Notes | |---|---|---| | CTRE Phoenix 6 (`com.ctre.phoenix6`, `TalonFX`) | 52 | the modern default | | CTRE Phoenix 5 (`com.ctre.phoenix.*`, legacy) | 51 | mostly older repos / Talon SRX | | REV (`com.revrobotics`, `SparkMax`/`SparkFlex`) | 49 | NEO ecosystem | | `TalonSRX` | 32 | legacy brushed/CIM | | `SparkFlex` / `CANSparkFlex` | 25 | NEO Vortex | | `ThriftyNova` | 2 | the long tail | The dominant call idioms, corpus-wide (raw text matches): - **`.set(...)`** — 8,626 occurrences. The legacy/duty-cycle setter (Phoenix 5, REV). - **`.setControl(request)`** — 2,620 occurrences. The Phoenix 6 idiom: build a request object (`VoltageOut`, `MotionMagicVoltage`, `PositionVoltage`, `TorqueCurrentFOC`, and so on) and hand it to the motor. Every motor abstraction in this chapter is, at bottom, a wrapper around `.setControl(request)`. A second number frames the alternative discussed later: WPILib's shared `MotorController` interface is imported by 23 teams but called only 47 times total, against `setControl` (CTRE) at 797 parsed call sites in the code index (the 2,620 above are raw text matches) and `setReference` (REV) at 301. Teams program the vendor's own control model, not a shared one. ## The spectrum of motor access There are two populations, and the gap between them is the whole point. Measured across the 63-team full clone: | Tier | Pattern | Teams (of 63) | |---|---|---| | Raw | Vendor type (`TalonFX`/`SparkMax`) instantiated directly inside `subsystems/` | 53 | | Partial IO | Some `*IO.java` interface exists (AdvantageKit-style) | 27 | | ↳ with sim impl | A `*IOSim.java` is present | 18 | | ↳ with logged inputs | `@AutoLog` / `*IOInputs` struct present | 23 | | Reusable `MotorIO` | A motor-generic IO reused across mechanisms | 6 | (The 27/63 here and chapter 16's 24/55 are the same IO-seam adoption pattern measured against two denominators — the full clone and the one-repo-per-team season index.) The 53-team raw majority treat the subsystem as the motor wrapper: the vendor object is a field, gear-ratio math is inline, and there is no seam where a simulated or replayed motor could be substituted. The six teams in the bottom row — 254, 1678, 971, 2910, 2637, 5137 — instead define one motor contract and implement it once per device family (`...TalonFX`, `...Sim`, `...SparkMax`). That single decision is what lets simulation, unit tests, and log replay attach later for free. The rest of this chapter reads those six contracts in full, then turns to the capability-typed variant. ## The six reusable contracts Read these as six answers to the same design brief. They cluster on a few axes, covered after, but the surface area is worth seeing first. ### 254 Cheesy Poofs A lean pure `interface`. Mechanism-unit `double`s on the hot path; one `int slot` for PID gain sets; `default` methods cascade the overloads down to one fully-specified primitive. The companion `MotorInputs` is a flat `@AutoLog` struct. ```java public interface MotorIO { void readInputs(MotorInputs inputs); void setOpenLoopDutyCycle(double dutyCycle); // These are in the "units" of the subsystem (rad, m). void setPositionSetpoint(double units); default void setMotionMagicSetpoint(double units) { setMotionMagicSetpoint(units, 0); } void setMotionMagicSetpoint(double units, int slot); default void setMotionMagicSetpoint( double units, double velocity, double acceleration, double jerk) { setMotionMagicSetpoint(units, velocity, acceleration, jerk, 0); } // ... cascades to the fully-specified primitive ... void setMotionMagicSetpoint( double units, double velocity, double acceleration, double jerk, int slot, double feedforward); void setNeutralMode(NeutralModeValue mode); default void setVelocitySetpoint(double unitsPerSecond) { setVelocitySetpoint(unitsPerSecond, 0); } void setVelocitySetpoint(double unitsPerSecond, int slot); void setVoltageOutput(double voltage); void setCurrentPositionAsZero(); void setCurrentPosition(double positionUnits); void setEnableSoftLimits(boolean forward, boolean reverse); void setEnableHardLimits(boolean forward, boolean reverse); void follow(CANDeviceId masterId, boolean opposeMasterDirection); void setTorqueCurrentFOC(double current); void setMotionMagicConfig(MotionMagicConfigs config); void setVoltageConfig(VoltageConfigs config); } ``` ```java @AutoLog public class MotorInputs { public double velocityUnitsPerSecond = 0.0; public double unitPosition = 0.0; public double appliedVolts = 0.0; public double currentStatorAmps = 0.0; public double currentSupplyAmps = 0.0; public double rawRotorPosition = 0.0; } ``` Note `setNeutralMode(NeutralModeValue mode)` and the `MotionMagicConfigs` / `VoltageConfigs` parameters: this is a clean command surface that still leaks CTRE config types in its signatures. Hold that thought for the alternatives section. ### 2910 Jack in the Bot Closest sibling to 254, with two telling differences. The `@AutoLog` inputs class is nested inside the interface, and it splits state into motor-frame (`motorRaw…`, rotor-relative) versus mechanism-frame (`mechanismRaw…`, post-ratio) — so logs show both the raw rotor and the geared output. Almost every method is a `default {}` no-op, making partial implementations legal: a roller impl never overrides the position methods. It also leaks two vendor getters, `getTalon()` and `getConfig()`. ```java public interface MotorIO { @AutoLog class MotorIOInputs { public boolean connected = false; // talon.getRotor...() — motor-frame, independent of ratios public double motorRawPositionRotations = 0.0; public double motorVelocityRPS = 0.0; public double motorVoltage = 0.0; public double motorStatorCurrentAmps = 0.0; public double motorSupplyCurrentAmps = 0.0; public double motorTemperatureC = 0.0; // talon.get...() — mechanism-frame, after the ratios public double mechanismRawPositionInMechanismUnits = 0.0; public double mechanismVelocityPerSecondInMechanismUnits = 0.0; } void updateInputs(MotorIOInputs inputs); BaseMotorConfig getConfig(); TalonFX getTalon(); double getMotorRotationsToMechanismUnitsRatio(); default void setNeutralMode(NeutralModeValue mode) {} default void setTorqueCurrentFOC(double current) {} default void setOpenLoopDutyCycle(double dutyCycle) {} default void setVoltageOutput(double voltage) {} default void follow(CanDeviceId leaderID, MotorAlignmentValue motorAlignment) {} default void setMechanismPositionSetpoint(double mechanismPosition) {} default void setMechanismPositionSetpoint(double mechanismPosition, int slot) {} default void setEncoderPositionAsZero() {} default void setMechanismVelocityPerSecondSetpoint(double mechanismVelocity) {} default void setMotionMagicSetpoint(double mechanismPosition, int slot) {} default void setMotionMagicSetpoint(double mechanismPosition, double velocity, double acceleration, double jerk) { setMotionMagicSetpoint(mechanismPosition, velocity, acceleration, jerk, 0); } // ... cascades ... default void setMotionMagicConfig(MotionMagicConfigs config) {} } ``` The motor-frame / mechanism-frame split is the distinctive idea here: when a log shows a position glitch you can tell whether the rotor or the gearbox math is at fault. ### 971 Spartan Robotics *(From 971's 2026 second-robot Java codebase — not their well-known C++/Bazel main stack.)* The minimalist, type-safe answer. An `abstract class`, not an interface, using WPILib `Units` measure types (`Voltage`, `Angle`, `AngularVelocity`, `Distance`) so a unit mismatch is a compile error. Note the deliberate `Angle` versus `Distance` overloads of `setPosition` / `resetPosition` — the same contract serves rotational and linear mechanisms. Telemetry is not a separate struct: Lombok `@Getter` plus AdvantageKit `@AutoLogOutput` fields publish state directly, and a concrete `periodic()` logs the converted values. ```java public abstract class MotorIO { protected final String name; @Getter protected final MotorConfig motorConfig; @Getter @AutoLogOutput(key = "{name}/Applied Voltage") protected Voltage appliedVoltage; @Getter @AutoLogOutput(key = "{name}/Supply Current") protected Current supplyCurrent; @Getter @AutoLogOutput(key = "{name}/Stator Current") protected Current statorCurrent; @Getter @AutoLogOutput(key = "{name}/Temperature") protected Temperature temperature; @Getter @AutoLogOutput(key = "{name}/Connected") protected boolean connected = false; @Getter protected Angle position = Rotations.of(0.0); @Getter protected AngularVelocity velocity = RotationsPerSecond.of(0.0); public void periodic() { Logger.recordOutput(name + "/Position", UnitUtil.toDouble(position, motorConfig.LOG_UNIT())); Logger.recordOutput(name + "/Velocity", UnitUtil.toDouble(velocity, motorConfig.LOG_UNIT().per(Seconds))); } public abstract void setVoltage(Voltage goalVoltage); public abstract void setVelocity(AngularVelocity goalVelocity); public abstract void setPosition(Angle goalPosition); public abstract void setPositionVoltage(Angle goalPosition); public abstract void setPosition(Distance goalPosition); // elevators public abstract void setPositionVoltage(Distance goalPosition); public abstract void resetPosition(Angle newPosition); public abstract void resetPosition(Distance newPosition); public abstract void setCoast(); } ``` This is the smallest surface of the six. Every method is `abstract`, so every implementation must satisfy the whole contract — no `default {}` escape hatch and no silent no-op. ### 2637 PhantomCatz A generic interface parameterized over its inputs type (``), so each subsystem can extend the inputs struct. Distinctive choices: the inputs use arrays (`appliedVolts[]`, `tempCelcius[]`) to log a leader plus its followers in one struct; it exposes `getSignals()` so the robot can batch-refresh all status signals with one CTRE `BaseStatusSignal.refreshAll(...)`; and it carries an explicit `enable()` / `disable()` / `stop()` lifecycle. Setters are documented as applied only through Setpoints — the control mode is chosen by a higher-level object, not called directly. Gains and motion-magic params are settable at runtime. ```java public interface GenericMotorIO { public static class MotorIOInputs { public boolean isLeaderConnected = false; public boolean[] isFollowerConnected = new boolean[] {}; public double position = 0.0; // latency-compensated public double velocityRPS = 0.0; public double[] appliedVolts = new double[] {}; public double[] supplyCurrentAmps = new double[] {}; public double[] torqueCurrentAmps = new double[] {}; public double[] tempCelcius = new double[] {}; } public default void updateInputs(T inputs) {} public default void setCurrentPosition(double mechanismPosition) {} public default BaseStatusSignal[] getSignals() { return new BaseStatusSignal[0]; } public default void zeroSensors() {} public default void setNeutralBrake(boolean wantsBrake) {} // Applied only through Setpoints: public default void setNeutralSetpoint() {} public default void setVoltageSetpoint(double voltage) {} public default void setMotionMagicSetpoint(double mechanismPosition) {} public default void setVelocityFOCSetpoint(double mechanismVelocity) {} public default void setDutyCycleSetpoint(double percent) {} public default void setPositionSetpoint(double mechanismPosition) {} public default void enable() {} public default void disable() {} public default void stop() {} public default void setGainsSlot0(double p, double i, double d, double s, double v, double a, double g) {} public default void setMotionMagicParameters(double velocity, double acceleration, double jerk) {} public default void setNeutralMode(TalonFX fx, NeutralModeValue neutralMode) {} public default void setNeutralMode(TalonFXS fx, NeutralModeValue neutralMode) {} } ``` The follower-array inputs and `getSignals()` batching are oriented at the CAN-bus cost of refreshing many status signals each loop. The trailing `setNeutralMode(TalonFX, …)` overloads, taking the vendor type as an argument, are the clearest leak in this interface. ### 5137 Iron Kodiaks The maximal, fully-instrumented contract — a concrete base class with the richest inputs struct in the corpus. It carries control-loop introspection (`error`, `propOutput`, `derivOutput`, `intOutput`, `feedforward`), faults, limit flags, and `encoderDiff` for encoder-fusion debugging. Two ideas worth studying. First, every optional method defaults to `unsupportedFeature()`, which raises a dashboard `Alert` rather than failing silently, so commanding an unsupported mode is visible at the driver station. Second, it owns `Alert`s for disconnect, hardware-fault, over-temp, and limits, refreshing them in `update()`. ```java public class MotorIO { @AutoLog public static class MotorIOInputs { public boolean connected; // Mechanism values: arms/flywheels use rad & rad/s; elevators use m & m/s public double position; public double velocity; public double accel; public double appliedVoltage; public double supplyCurrent; public double torqueCurrent; public String controlMode; // DutyCycle, Voltage, MotionMagic, ... public double setpoint; public double error; // target - position public double feedforward; public double derivOutput; // kD contribution public double intOutput; // kI contribution public double propOutput; // kP contribution public double temp; public double encoderDiff; // encoder vs motor, for sync visualization public boolean hitForwardLimit; public boolean hitReverseLimit; public boolean hardwareFault; public boolean tempFault; } public void update() { Logger.processInputs(logPath, inputs); disconnectAlert.set(!inputs.connected); hardwareFaultAlert.set(inputs.hardwareFault); tempFaultAlert.set(inputs.tempFault); forwardLimitAlert.set(inputs.hitForwardLimit); reverseLimitAlert.set(inputs.hitReverseLimit); } private void unsupportedFeature() { if (Constants.currentMode != Mode.REPLAY) Alerts.create("An unsupported feature was used on " + getName(), AlertType.kWarning); } // ---- Open-loop ---- public void setDutyCycle(double value) { unsupportedFeature(); } public void setVoltage(double volts) { unsupportedFeature(); } public void setTorqueCurrent(double current){ unsupportedFeature(); } // ---- Closed-loop position (Motion Magic & direct), current- or voltage-based ---- public void setGoalWithCurrentMagic(double goal) { unsupportedFeature(); } public void setGoalWithVoltageMagic(double goal) { unsupportedFeature(); } public void setVelocityWithCurrent(double velocity) { unsupportedFeature(); } public void setVelocityWithVoltage(double velocity) { unsupportedFeature(); } // ---- Gains (individual + whole-slot) ---- public void setkP(double kP) { unsupportedFeature(); } public void setkD(double kD) { unsupportedFeature(); } public void setGains(Slot0Configs gains) { unsupportedFeature(); } // ---- Encoder fusion & zeroing ---- public void connectEncoder(EncoderIO encoder, double motorToSensorRatio, boolean fuse) { unsupportedFeature(); } public void setPosition(double position) { unsupportedFeature(); } // ---- Simulation-only ---- public void setMechPosition(double position) { unsupportedFeature(); } public void disconnect() { unsupportedFeature(); } } ``` The `unsupportedFeature()` pattern is the inverse of 971's all-`abstract` choice: instead of forcing every impl to satisfy the whole contract, it lets impls skip what they cannot do, and surfaces the gap as a runtime alert when something calls the missing method. The `Slot0Configs gains` parameter is again a CTRE type in the signature. ### 1678 Citrus Circuits The most architecturally ambitious of the six: an `abstract class implements Sendable` (778 lines) where control is reified as an immutable `Setpoint` value object. Subsystems never call `setVelocitySetpoint` directly — those are `protected`. Instead they build a `Setpoint` (`Setpoint.withMotionMagicSetpoint(angle)`, `withVelocitySetpointAndCurrentLimit(v, …)`) and call `applySetpoint(setpoint)`. The base class owns an `enabled` flag, the reapply-last-setpoint-on-enable behavior, a `Mode` enum classifying control types, units baked into the instance, multi-follower input tracking, and full `Sendable` dashboard wiring. ```java public abstract class MotorIO implements Sendable { public final AngleUnit unitType; public final TimeUnit time; protected final Inputs inputs; protected final Inputs[] followerInputs; private Setpoint setpoint = Setpoint.withNeutralSetpoint(); private boolean enabled = true; public abstract void updateInputs(); public abstract void setCurrentPosition(Angle mechanismPosition); public abstract void zeroSensors(); public abstract void setNeutralBrake(boolean wantsBrake); public abstract TalonFXConfiguration getMotorIOConfig(); public abstract void useSoftLimits(boolean enable); // protected — only invoked via a Setpoint's applier: protected abstract void setNeutralSetpoint(); protected abstract void setVoltageSetpoint(Voltage voltage); protected abstract void setMotionMagicSetpoint(Angle mechanismPosition, int slot); protected abstract void setVelocitySetpoint(AngularVelocity mechanismVelocity, int slot); protected abstract void setDutyCycleSetpoint(Dimensionless percent); protected abstract void setPositionSetpoint(Angle mechanismPosition, int slot); // --- concrete lifecycle: the heart of the design --- public final void applySetpoint(Setpoint setpointToApply) { setpoint = setpointToApply; if (enabled) setpointToApply.apply(this); } public final void enable() { enabled = true; setpoint.apply(this); } public final void disable() { enabled = false; Setpoint.withNeutralSetpoint().apply(this); } } ``` The control-mode classifier and the reified setpoint: ```java public enum Mode { IDLE, VOLTAGE, MOTIONMAGIC, VELOCITY, DUTY_CYCLE, POSITIONPID; public boolean isPositionControl() { /* MOTIONMAGIC, POSITIONPID */ } public boolean isVelocityControl() { /* VELOCITY */ } } public static class Setpoint { private final UnaryOperator applier; // how to push this onto the IO public final Mode mode; public final double baseUnits; // target, in WPILib base units static Setpoint withNeutralSetpoint(); static Setpoint withVoltageSetpoint(Voltage v); static Setpoint withMotionMagicSetpoint(Angle p); static Setpoint withMotionMagicSetpointAndCurrentLimit(Angle p, Current maxStator, Current maxSupply); static Setpoint withVelocitySetpoint(AngularVelocity v); static Setpoint withVelocitySetpointAndCurrentLimit(AngularVelocity v, Current maxStator, Current maxSupply); static Setpoint withCustomSetpoint(UnaryOperator applier, Mode mode, double baseUnits); public void apply(MotorIO io) { applier.apply(io); } } ``` The `…AndCurrentLimit` / `…AndVoltageLimit` factories mutate the `TalonFXConfiguration` in place before applying the setpoint — the setpoint can carry a transient config change, for example "go to this position but cap stator at 40 A." That is the most powerful idea in the six, and the hardest to retrofit. The cost is the matching one: `getMotorIOConfig()` returns a `TalonFXConfiguration`, so CTRE's config type is baked into the abstract base. ## The design axes they disagree on The six cluster on a handful of decisions. Reading across them is more useful than any one in isolation. | Axis | Lean (254, 2910, 5137) | Type-safe (971, 1678) | |---|---|---| | Form | `interface` (254, 2910, 2637) | `abstract class` (971, 1678, 5137) | | Units | raw `double`, mechanism units by convention | WPILib `Units` (`Angle`, `Voltage`, …), compile-checked | | Control entry | direct setters (`setVoltageOutput`, `setVelocitySetpoint`) | reified `Setpoint` value object (1678, 2637) | | Inputs | flat `@AutoLog` struct (254, 5137, 2637, 2910) | `@AutoLogOutput` fields + `periodic()` (971) | | Optional methods | `default {}` no-op (2910, 2637) or `unsupportedFeature()` alert (5137) | `abstract` — every impl must satisfy (254, 971, 1678) | | Motor vs mechanism frame | mostly mechanism-only; 2910 logs both | mechanism-frame (gear math in impl) | | Followers | `follow(id, oppose)` call | `followerInputs[]` array + per-follower logging (1678, 2637) | | Slots | `int slot` parameter for multiple gain sets (254, 2910, 2637) | per-`Setpoint` (1678) | Several themes are stable across all six. **What crosses the line.** The command surface is mostly clean — mechanism-unit doubles or WPILib measure types in, no `StatusSignal` out. But config crosses in every one of them: `MotionMagicConfigs` and `VoltageConfigs` (254), `BaseMotorConfig` and `getTalon()` (2910), `Slot0Configs` (5137), `TalonFXConfiguration` (1678), and `setNeutralMode(TalonFX, …)` (2637). Commands abstract well; configuration resists. **Loop placement.** Every contract assumes the control loop runs on the motor controller, not the roboRIO. The verbs are `setMotionMagicSetpoint`, `setPositionSetpoint`, `setVelocityFOCSetpoint` — they hand a target plus gains to the device and let its onboard PID close the loop. None of the six runs a software PID across the seam. **Units.** Two camps: raw `double` in mechanism units by convention (254, 2910, 2637, 5137), or WPILib `Units` measure types that make a unit mismatch a compile error (971, 1678). The convention camp is terser; the type camp catches the rad-versus-rotation bug at build time. **Config versus command.** The cleanest split is 1678's, where a `Setpoint` is the only way to command, and config-changing setpoints (`…AndCurrentLimit`) are a distinct, named subset. The looser designs mix runtime config setters (`setGainsSlot0`, `setMotionMagicParameters`) into the same flat surface as the commands. **Capability tiers.** Designs handle "this motor cannot do that" three ways: make everything `abstract` so every impl is complete (971, 1678, 254); make everything a `default {}` no-op so partial impls are legal and silent (2910, 2637); or default to `unsupportedFeature()` so a missing method raises a visible alert (5137). The consensus telemetry, present in nearly every inputs struct: `connected`, `position`, `velocity`, `appliedVolts`, `statorCurrent`, `supplyCurrent`, `tempCelsius`, `rawRotorPosition`. The consensus command verbs: open-loop (duty cycle, voltage, torque-current FOC); closed-loop (position PID, profiled position via Motion Magic, velocity — each with an optional gain slot and feedforward); neutral (brake / coast); config (gains, motion constraints, current limits, soft limits); sensor (zero, set-current-position, encoder fusion); topology (follow); and sim hooks (force position/velocity, force-disconnect). ## Capability-typed devices The six contracts above abstract "a motor" — one interface, many control modes, and config types leaking through. The alternative seen in [Part I's alternatives entry](../part-1/08-alternatives.md) abstracts a **role** instead. It is shown here in full because it is the cleanest answer to the leak the survey exposes. The rule that makes it work: the interface name describes a capability the caller depends on, not the hardware that provides it. - Avoid `ITalonMotor`, `ICTREFOCMotor`, `SparkPositionMotor` — vendor leaks into the name; callers couple to a brand. - Prefer `PositionMotor`, `VelocityMotor`, `TorqueMotor` — named for what you ask of them. Split by capability rather than building one universal `Motor` god-interface. A device implements the set of role interfaces it can honor. ```java public enum NeutralMode { BRAKE, COAST } // a neutral concept, not NeutralModeValue / IdleMode /** A motor you command to a mechanism position. Units are mechanism units (rad/m), not rotor rotations. */ public interface PositionMotor { void setPosition(double positionRad); // go to / hold double getPositionRad(); double getVelocityRadPerSec(); void setVoltage(double volts); // open-loop escape hatch void setNeutralMode(NeutralMode mode); } /** A motor you command to a speed. */ public interface VelocityMotor { void setVelocity(double radPerSec); double getVelocityRadPerSec(); void setVoltage(double volts); void setNeutralMode(NeutralMode mode); } ``` Two non-negotiables: signatures speak mechanism units and neutral enums. The moment a `NeutralModeValue`, a `Rotation`, or a `StatusSignal` appears in the interface, the abstraction has failed and you have renamed a `TalonFX`. This is exactly the config-leak the six corpus contracts all show; here the rule is enforced by keeping config out of the signature entirely. ### FOC as an orthogonal opt-in FOC is an efficiency optimization — "do the same job, better" — so it should be optional and orthogonal, not baked into the control interface. The design splits it into two distinct concepts. The efficiency aspect becomes an optional mix-in capability. It changes how well the job is done, not what you command, so it is a separate interface a device implements only if it has it: ```java /** Opt-in efficiency mode (e.g. FOC). Absent where unsupported — query, don't assume. */ public interface EfficiencyOptimizable { void setEfficiencyOptimization(boolean enabled); } ``` A caller writes `if (motor instanceof EfficiencyOptimizable o) o.setEfficiencyOptimization(true);`. A motor that cannot do it does not implement the interface — capability presence lives in the type system, not a runtime flag that silently lies. The torque-control aspect is a different verb, not an optimization, so it is its own role interface: ```java public interface TorqueMotor { // CTRE FOC today; whoever ships it tomorrow void setTorqueCurrent(double amps); } ``` Today only CTRE implements `TorqueMotor` and `EfficiencyOptimizable`. Abstracting over one implementation is cheap and forward-looking: when a second vendor ships FOC, you add an implementation and no caller changes. ### Vendor types live in the impl, fully Below the interface, each impl talks to its device in the device's native language and uses its best features: ```java final class TalonFXMotor // one impl offers every role the device can honor implements PositionMotor, VelocityMotor, TorqueMotor, EfficiencyOptimizable { private final TalonFX talon; // vendor type, below the line private final MotionMagicVoltage posReq = new MotionMagicVoltage(0).withEnableFOC(true); public void setPosition(double rad) { talon.setControl(posReq.withPosition(rad / rotToRad)); // Motion Magic + FOC } public void setTorqueCurrent(double amps) { talon.setControl(new TorqueCurrentFOC(amps)); } public void setEfficiencyOptimization(boolean on) { posReq.EnableFOC = on; } } final class SparkMaxPositionMotor implements PositionMotor { /* MAXMotion; NO FOC → fewer interfaces */ } final class SimPositionMotor implements PositionMotor { /* physics plant, no vendor at all */ } ``` `SparkMaxPositionMotor` implements only `PositionMotor` — it does not pretend to be a `TorqueMotor`. The interface set a device offers is its honest capability advertisement. No lowest-common-denominator flattening, no emulation that lies. ### The hardware object, briefly One `Hardware` object per robot variant constructs and configures every device — through a `TalonFXFactory.create(id, spec)`-style configured factory — and hands each subsystem only the capability interface it needs: `Elevator(PositionMotor motor)`, never the concrete type and never `Hardware` itself. That injection discipline is the line between this and the old `RobotMap` god-object, and the `Hardware.create(mode, cfg)` factory doubles as the run-mode switch: REAL, SIM, and REPLAY each resolve every interface in one place, without any vendor type crossing into a subsystem. The fully worked build — the `Hardware` class, the per-robot spec objects, and the factory — is in [Part I, chapter 8](../part-1/08-alternatives.md) and [Part III, chapter 26](../part-3/26-portable-motor-interface.md). ### The corpus reality check The clean form is rare. Measured across the season repos: a device-level motor abstraction shows up in roughly 10 teams — the `MotorIO` interface in 254 and 2910; the `MotorIO` class in 1678, 5137, and 971; and a `Motor` type in 971, 2412, 4099, 4504, 5026, and 4738. But almost all of them leak vendor types, as the survey above showed in detail: 254's `MotorIO` imports `com.ctre.phoenix6` `MotionMagicConfigs` and `NeutralModeValue`. The vendor-clean, capability-segregated form described here is the rare case — which is why it is an alternative, not the default. The supporting infrastructure, though, is common: configured-device factories such as `TalonFXFactory` (6 teams including 254, 1678, 3061), `PhoenixUtil` (8 teams), and `CTREConfigs` (7 teams). That is the layer this pattern sits on — the `TalonFXFactory.create(id, spec)` call inside the impl above. ## When the lower seam pays, and when it is over-engineering Reach for capability-typed devices when: - You have several mechanically-similar mechanisms — elevator plus arm plus wrist all reduce to "one motor, position control" — and want to share one implementation and one `SimPositionMotor` across all of them. - You run multiple physical robots, or anticipate a vendor change, and want the swap to be a new impl rather than a subsystem rewrite. - You want the FOC decision expressed once, in the type system, rather than re-litigated at each call site. Do not, when: - You are single-vendor with a handful of mechanisms. The per-subsystem `XxxIO` seam from Part I is less machinery for the same sim/test/replay payoff. - You would be abstracting capabilities you do not actually command. Speculative generality is the failure mode here — build `PositionMotor` because your arm needs it, not a universal motor API on spec. The honest costs are three. You still write a new impl per vendor — the abstraction protects callers, not the impl author. The neutral-spec-to-vendor-config mapper is ongoing maintenance as vendordeps evolve. And it is slightly contrarian to Part I, which draws the boundary at the subsystem. Both are valid; the lower seam trades more device-level machinery for cross-mechanism reuse and a clean cross-vendor future. They compose: capability-typed devices can live inside subsystem IO impls. Writing your own motor class is sensible when it is an interface on top of a vendor device, named for a capability, with the configuration for one purpose wired in. Split it by role, model FOC as an orthogonal opt-in, let each device advertise its real capability set through the interfaces it implements, and let one hardware object construct, configure, and inject them as narrow references. Vendor names stay out of every signature; vendor code stays sealed inside the impls. Part III takes this evidence and proposes one portable interface built on it. ## Related chapters - [15. The control path](15-control-path.md) - [16. Hardware abstraction](16-hardware-abstraction.md) - [18. Subsystem archetypes](18-subsystem-archetypes.md) - [19. The drivetrain subsystem](19-the-drivetrain-subsystem.md) - [20. The world model](20-the-world-model.md) - [21. Vision systems](21-vision-systems.md) - [22. Coordination: state machines](22-coordination-state-machines.md) - [23. Coordination: graphs and trees](23-coordination-graphs-trees.md) Next: [18. Subsystem archetypes](18-subsystem-archetypes.md) # 18. Subsystem archetypes https://frc-code-scout.jointheleague.org/part-2/18-subsystem-archetypes/ *Part I motivated the IO quartet in the abstract; this chapter applies it per mechanism. Game mechanisms — elevator, arm, shooter, intake — collapse to a small set of control archetypes, and the archetype, not the game name, decides the IO contract, the WPILib sim model, and the test. This chapter walks the four actuating archetypes, each with its sim model, an abridged real example, the one control decision it forces, and the testability point it turns on.* [Chapter 3](../part-1/03-the-io-seam.md) introduced the IO seam and the four-file quartet that hangs off it: an interface, a hardware implementation, a sim implementation, and the subsystem that holds one of them. That chapter argued the seam is worth its cost. It deferred the per-mechanism detail to here. This is that detail — the same seam drawn four times, once for each control type a robot actually builds. The companion chapters in this part fill in the surrounding machinery: the [control path](15-control-path.md) that runs above the seam, [hardware abstraction](16-hardware-abstraction.md) and [motor interfaces](17-motor-interfaces.md) that sit at and below it. The two seams that have no motors at all — the [world model](20-the-world-model.md) and the coordination layer ([state machines](22-coordination-state-machines.md), [graphs and trees](23-coordination-graphs-trees.md)) — are their own chapters. Vision, a sensor-only subsystem, is [chapter 21](21-vision-systems.md). The [drivetrain](19-the-drivetrain-subsystem.md), a special case with two interfaces and kinematics, comes next. --- ## The shared template A subsystem is usually named after the game mechanism, but mechanisms collapse to a handful of control archetypes. The archetype determines three things: the shape of the IO contract, the WPILib simulation model that backs the fake hardware, and the unit test you can write. So the material is organized by archetype, each led by its most recognizable mechanism. | Archetype | Mechanisms | Sim model | Contract shape | |---|---|---|---| | linear position | **Elevator**, Climber | `ElevatorSim` | position in, volts out | | rotational position | **Arm**, Pivot, Wrist, Turret | `SingleJointedArmSim` | angle in, volts out | | velocity | **Shooter**, Flywheel | `FlywheelSim` | speed in, volts out | | roller + sensor | **Intake**, Indexer, Feeder, Manipulator | `DCMotorSim` + beam-break | run / stop + "have piece?" | If a mechanism is not listed by name, find its archetype. Anything that goes to a height is linear position; anything that goes to an angle is rotational position; anything that spins to a speed is velocity; anything that moves game pieces past a sensor is a roller. ### The quartet, per archetype Every subsystem in this chapter is the same four-file quartet — contract, device-named hardware impl, sim impl, subsystem — whose canonical shape and naming conventions are [chapter 16's](16-hardware-abstraction.md#naming-the-implementations). Two recurring companions ride along: an **`XxxIOInputs` struct** carries the readings, often `@AutoLog`-annotated so AdvantageKit logs and replays them, and a **no-op / null** implementation (`NoXxx` / `XxxIONull`) lets the robot run with the mechanism disabled. What changes per archetype is only what crosses the line and which WPILib model backs the fake hardware. ### The ethic, applied [Chapter 16](16-hardware-abstraction.md#why-confinement-pays) states the ethic canonically: mock below and test above, keep the package liftable as a library, and never let a vendor type cross above the IO line — three views of the same property. The sections below do not restate it; they apply it to each archetype's specifics. One mechanism is the deliberate exception to the quartet: an **LED / status** subsystem has no sensor feedback and no control loop, only an output buffer the rest of the robot writes patterns to. Build it as a thin output sink wrapping an `AddressableLED`; do not force the quartet onto it. --- ## Linear position — the reference archetype A linear position mechanism moves a carriage to a commanded **height** along a rail and holds it against gravity. The robot has one if a thing goes up and down to setpoints: an **elevator** (the canonical case) or the extension stage of a **climber**. The job is "get to height *h* and stay there," so the subsystem reduces to a position controller plus a gravity feedforward. The corpus shows 17 of the 55 season-index teams with a clean `ElevatorIO` + `ElevatorIOSim` and 19 with a climber IO; an elevator doc and a climber doc would be about 85% the same. This is the reference archetype — the other three are described as deltas against it. ### The control decision State is **position** (meters) and its derivative **velocity** (m/s). Control is feedback on position plus a feedforward for gravity: a profiled PID drives the error to zero while a constant `kG` term holds the carriage up. The one decision the archetype forces is **where the loop lives** — above or below the IO line: - **Loop above the line** (recommended for a mechanism you simulate). The interface is a *device pipe*: `setVoltage(v)` plus `position()` / `velocity()` getters. The subsystem owns the single `ProfiledPIDController` and `ElevatorFeedforward`. Sim and Real run the **same** controller, so they stay in parity for free. SciBorgs (1155) builds it this way. - **Loop below the line** (use to exploit firmware control). The interface is an *intent contract*: `runPosition(rad, ff)` / `runToHeight(h)`, and each implementation runs its own loop, typically Phoenix 6 MotionMagic on the motor. 6328 and 3636 do this. The cost: the sim impl must re-implement the loop, and the two can drift. For a mechanism you intend to test in sim, prefer loop-above so the test exercises the controller that runs on the robot. ### The sim model WPILib's **`ElevatorSim`** — a physics model of a gravity-loaded carriage on a motor and gearbox. It is the single reason this subsystem is testable: the sim impl wraps it, and a test steps it. ### The contract The loop-above interface, from SciBorgs (1155), Reefscape 2025, `robot/elevator/ElevatorIO.java`: ```java public interface ElevatorIO extends AutoCloseable { /** @param voltage Voltage inputted to gearbox. */ public void setVoltage(double voltage); /** @return The encoder value in meters. */ public double position(); /** @return The encoder value in meters per second. */ public double velocity(); /** Resets the elevator encoder to a measurement of 0. */ public void resetPosition(); } ``` Twelve lines, no imports at all — not even WPILib. That is the spine. The contract deliberately omits the vendor type, the game logic ("score L4"), and any other subsystem. It is only volts out and meters in. The loop-below style replaces `setVoltage` with `runPosition(positionRad, feedforward)` and adds `setPID(...)`. When a team logs with an inputs struct instead of plain getters, the same readings appear as fields: `positionRad` / `leftHeight`, `velocityRadPerSec`, `appliedVolts`, `supplyCurrentAmps` (stall / jam detection), `tempCelsius`, and `motorConnected` (so the robot degrades safely if a motor is unplugged). ### The hardware implementation The one file where a vendor type appears, from `robot/elevator/RealElevator.java`: ```java import com.ctre.phoenix6.hardware.TalonFX; // ◀ vendor import — allowed HERE, nowhere above import com.ctre.phoenix6.controls.Follower; // ... public class RealElevator implements ElevatorIO { private final TalonFX leader = new TalonFX(FRONT_LEADER, CANIVORE_NAME); private final TalonFX follower = new TalonFX(BACK_FOLLOWER, CANIVORE_NAME); public RealElevator() { follower.setControl(new Follower(FRONT_LEADER, true)); // ...brake mode, current limit, sensor-to-mechanism ratio... } @Override public void setVoltage(double voltage) { leader.setVoltage(voltage); } @Override public double position() { return leader.getPosition().getValueAsDouble(); } @Override public double velocity() { return leader.getVelocity().getValueAsDouble(); } @Override public void resetPosition() { leader.setPosition(0); } } ``` `com.ctre` lives in this file and only this file. Swapping to a SparkMax means writing one new `ElevatorIO` impl; the subsystem, the test, and the rest of the robot never know. ### The sim implementation The same four methods, backed by physics instead of a motor, from `robot/elevator/SimElevator.java`: ```java public class SimElevator implements ElevatorIO { private final ElevatorSim elevator = new ElevatorSim( LinearSystemId.createElevatorSystem( DCMotor.getKrakenX60(2), WEIGHT.in(Kilograms), SPROCKET_RADIUS.in(Meters), GEARING), DCMotor.getKrakenX60(2), MIN_EXTENSION.in(Meters), MAX_EXTENSION.in(Meters), true, MIN_EXTENSION.in(Meters)); @Override public void setVoltage(double voltage) { elevator.setInputVoltage(voltage); elevator.update(Constants.PERIOD.in(Seconds)); // advance the physics one tick } @Override public double position() { return elevator.getPositionMeters(); } @Override public double velocity() { return elevator.getVelocityMetersPerSecond(); } @Override public void resetPosition() { elevator.setState(0, 0); } } ``` Imports are WPILib only. This file is the entire reason the subsystem is testable on a laptop. ### The subsystem The subsystem holds one `ElevatorIO`, owns the loop, and never names a motor type, from `robot/elevator/Elevator.java`: ```java public class Elevator extends SubsystemBase implements AutoCloseable { private final ElevatorIO hardware; private final ProfiledPIDController pid = new ProfiledPIDController(kP, kI, kD, /*constraints*/); private final ElevatorFeedforward ff = new ElevatorFeedforward(kS, kG, kV, kA); // the selection point: one place chooses the implementation public static Elevator create() { return new Elevator(Robot.isReal() ? new RealElevator() : new SimElevator()); } public static Elevator none() { return new Elevator(new NoElevator()); } public Command goTo(double height) { return run(() -> update(height)).finallyDo(() -> hardware.setVoltage(0)); } private void update(double goal) { // the loop lives ABOVE the line double feedback = pid.calculate(hardware.position(), goal); double feedforward = ff.calculateWithVelocities(/*...*/); hardware.setVoltage(feedback + feedforward); // only volts cross down } } ``` `create()` is the single selection point; `update()` is the controller both Real and Sim share. ### Variations | Variation | Team | How it differs | |---|---|---| | `@AutoLog` inputs + loop below | 6328 | interface is `runPosition(rad, ff)` / `runVolts`; the TalonFX runs the position loop | | Typed units (Kotlin) | 3636 | methods take `Distance` / `Voltage`; `runToHeight(Distance)` via MotionMagic; compile-time unit safety | | Null object | 1155 | `NoElevator` does nothing, so the robot runs with the mechanism unplugged (`Elevator.none()`) | | **Climber** = linear + load | 190 | same shape, simpler contract; `@AutoLog` inputs add an `isClimbed` latch | Treat a **climber** as this archetype with three deltas: the load is one-directional (gravity always pulls the robot down once hooked), so a holding current or brake matters more than a tuned profile; many add a ratchet, so the IO exposes a `setRatchet` or the real impl ignores reverse voltage; and a `boolean isClimbed` / `atTop` input is the signal the Superstructure reads. The physics model stays `ElevatorSim` (or a coarse `DCMotorSim`). ### The testability point Because `SimElevator` is just another `ElevatorIO`, the subsystem is unit-testable with zero hardware and zero other subsystems. This is SciBorgs' real test, from `ElevatorTest.java`: ```java public class ElevatorTest { private Elevator elevator; @BeforeEach public void initialize() { setupTests(); elevator = new Elevator(new SimElevator()); // ◀ mock the layer below } @AfterEach public void destroy() throws Exception { reset(elevator); } @ParameterizedTest @MethodSource("providePositionValues") public void reachesPosition(Distance height) { runUnitTest(elevator.goToTest(height)); // ◀ assert the layer above reaches setpoint } } ``` `new Elevator(new SimElevator())` is the entire trick: construct the real subsystem against fake hardware, command a height, step the sim, assert it arrived. `goToTest(height)` returns a `Test` that runs `goTo(height)` to completion and asserts `position ≈ height` — the same routine doubles as an on-robot system check. One note on the helpers: `setupTests()`, `runUnitTest()`, `fastForward()`, and `reset()` are not WPILib — they are SciBorgs' (1155) own `TestingUtil`, a thin sim-time-stepping and assertion wrapper you build once and reuse in every test in this chapter. One vendor-discipline caution this archetype illustrates: even SciBorgs leaks. Their `Elevator.java` carries `import com.ctre.phoenix6.SignalLogger;` to dump SysId state — a vendor type above the line in the subsystem itself. It is minor, but it is exactly the leak the corpus shows in 22 of 24 IO-layer teams. The fix is to route SysId state through a logging facade or confine `SignalLogger` to the real impl, and to enforce confinement with a checkstyle / spotless import rule rather than good intentions. --- ## Rotational position — angle plus an absolute encoder A rotational position mechanism drives a joint to a commanded **angle** and holds it. The robot has one wherever something rotates to setpoints: an **arm** or **pivot** (a shoulder), a **wrist** (a second joint at the end of an arm), or a **turret** (continuous yaw). The job is "go to angle θ and hold" — the same shape as the elevator, with rotation in place of translation. The corpus shows 14 teams with a clean `ArmIO` + `ArmIOSim` and 13 for a pivot. This is the elevator's twin. Two things make it harder. ### The control decision State is **angle** (radians) and angular velocity (rad/s); control is profiled PID plus feedforward. Two differences from the linear case shape the contract: - **Gravity is angle-dependent.** An elevator's `kG` is constant; an arm's gravity torque scales with `cos(θ)` — maximum horizontal, zero vertical. Use `ArmFeedforward` (which takes the angle), not `ElevatorFeedforward`. - **Absolute position matters.** You cannot reliably home an arm to a hard stop. Real arms use an **absolute encoder** (a CANcoder or through-bore) so the joint knows its true angle on boot. This is the rotational subsystem's signature hardware detail. The loop-above / loop-below decision is the same as linear, and the recommendation is the same: loop above for anything you simulate, so Sim and Real share one controller. ### The sim model WPILib **`SingleJointedArmSim`** — a pendulum on a motor and gearbox. It needs the arm's **moment of inertia** and length, harder to estimate than an elevator's mass. A wrong MOI is the usual reason a rotational sim test will not settle. ### The contract From SciBorgs (1155), Reefscape 2025, `robot/arm/ArmIO.java`: ```java public interface ArmIO extends AutoCloseable { /** @return The position in radians. */ public double position(); /** @return The position in radians/sec. */ public double velocity(); /** Sets the voltage of the arm motor. */ public void setVoltage(double voltage); } ``` Three methods — smaller than the elevator's, because a rotational joint with an absolute encoder rarely needs `resetPosition()`. `position()` must read the **absolute** encoder, not a relative count. The contract omits the `TalonFX` / `CANcoder` type, the scoring-level logic, and any other subsystem. The loop-below variant replaces `setVoltage` with `setAngle(rad)` / `runSetpoint(...)`. ### The hardware implementation The vendor type and the absolute encoder, both confined, from `robot/arm/RealArm.java`: ```java import com.ctre.phoenix6.hardware.TalonFX; // ◀ vendor — only here import com.ctre.phoenix6.signals.FeedbackSensorSourceValue; // ... public class RealArm implements ArmIO { private final TalonFX leader; public RealArm() { leader = new TalonFX(ARM_PIVOT, CANIVORE_NAME); config.Feedback.FeedbackSensorSource = FeedbackSensorSourceValue.RemoteCANcoder; // absolute angle config.Feedback.FeedbackRemoteSensorID = CANCODER; // ...current limits, brake mode... leader.getConfigurator().apply(config); } @Override public double position() { return leader.getPosition().getValue().in(Radians); } @Override public double velocity() { return leader.getVelocity().getValue().in(RadiansPerSecond); } @Override public void setVoltage(double voltage) { leader.setVoltage(voltage); } } ``` The CANcoder is used as the TalonFX's remote feedback source (`RemoteCANcoder`; `FusedCANcoder` if fused with the rotor sensor) — all of it inside the real impl, so "we switched to a through-bore on a SparkMax" is one new file. (A stray, unused `import com.revrobotics.spark.SparkMax;` lingers in this file — harmless, but exactly what an unused-import lint rule catches.) ### The sim implementation The only line that changes from the elevator, from `robot/arm/SimArm.java`: ```java public class SimArm implements ArmIO { private final SingleJointedArmSim sim = new SingleJointedArmSim( GEARBOX, GEARING, MOI, ARM_LENGTH.in(Meters), MIN_ANGLE.in(Radians), MAX_ANGLE.in(Radians), true, DEFAULT_ANGLE.in(Radians)); @Override public double position() { return sim.getAngleRads(); } @Override public double velocity() { return sim.getVelocityRadPerSec(); } @Override public void setVoltage(double voltage) { sim.setInputVoltage(voltage); sim.update(Constants.PERIOD.in(Seconds)); } } ``` Swap `ElevatorSim` for `SingleJointedArmSim` and meters for radians; the rest of the archetype is identical. The subsystem is structurally the elevator's twin — one `ArmIO`, a `ProfiledPIDController`, a `create()` selection point, a `goToTest(angle)` system check — with one change: the feedforward is `ArmFeedforward.calculate(angleRad, velocity)` so the gravity term tracks `cos(θ)`. ### Variations | Mechanism | Team | How it differs | |---|---|---| | Pivot (shoulder) | 1155 | identical to Arm; separate `PivotIO` / `RealPivot` / `SimPivot` / `NoPivot` package | | `@AutoLog` + loop-below | 5026, 2910 | `ArmIOInputs` struct, on-motor control, `setAngle` / `runSetpoint` contract | | Wrist (second joint) | 6328 | same archetype mounted on the arm; its zero is relative to the arm, so its feedforward needs the arm angle too | | **Turret** (continuous yaw) | 2637, 254 | no gravity term, but angle is **continuous** — use `MathUtil.angleModulus` or a continuous-input PID so it takes the short way around | The big split inside this archetype is gravity vs. no gravity: arm, pivot, and wrist need `ArmFeedforward`; a turret on a horizontal axis needs none but must handle wrap-around. ### The testability point The test is the elevator's shape — construct the subsystem on `SimArm`, command angles, assert it arrives — but the rotational archetype teaches an honest caution. SciBorgs **disabled** their `ArmTest`: ```java @Disabled // "Doesn't work :/" ◀ honest, and instructive public class ArmTest { Arm arm; @BeforeEach public void setup() { setupTests(); arm = Arm.create(); } @Test public void fullExtension() { runUnitTest(arm.goToTest(MIN_ANGLE)); runUnitTest(arm.goToTest(MAX_ANGLE)); } @RepeatedTest(5) public void randExtension() { runUnitTest(arm.goToTest(Radians.of(Math.random() * range).plus(MIN_ANGLE))); } } ``` A sim test is only as good as its physics constants. An arm's moment of inertia is hard to estimate; if the MOI or `ArmFeedforward` gains are off, the sim arm overshoots or never settles inside the tolerance and the test flakes. The fix is not to delete the test but to measure MOI (from CAD or a pendulum swing) and tune the sim — at which point you have a real regression test *and* a validated feedforward. The pattern (`new Arm(new SimArm())`, command, assert) is correct; the discipline is in the constants. Coordination — don't swing the arm into the elevator — lives in the `Superstructure`, not the arm. The arm neither knows nor cares that a wrist or an elevator exists, which is exactly why the arm package is liftable on its own. --- ## Velocity — reach a speed and hold it A velocity mechanism spins a wheel up to a commanded **speed** and holds it while energy is drawn off — a game piece launched, a roller fed. The robot has one wherever a **flywheel** or **shooter** must reach an RPM before it acts. The job is "get to ω rad/s and stay within tolerance," so the subsystem is a velocity controller with a `kV` feedforward: no position, no gravity. The corpus shows 14 teams with a `ShooterIO` and sim, 9 with a `FlywheelIO` and sim. The single most-forked example in the corpus is the 6328 AdvantageKit flywheel template, copied verbatim into many repos including 5712's. ### The control decision State is **angular velocity** (rad/s). Control is feedback on velocity plus a `kS` / `kV` feedforward (`SimpleMotorFeedforward`) — not `ElevatorFeedforward` or `ArmFeedforward`, because there is no gravity term. The defining property is the **"at speed" tolerance**: you act (shoot, feed) only when `|ω − ωₜ| < tolerance`, so the subsystem exposes an `atSetpoint()` or velocity reading the coordinator polls. The control decision here is different from the position archetypes. Velocity loops run cleanly **on the motor** (Phoenix 6 `VelocityVoltage`, Spark closed-loop), so the common contract is a hybrid: the subsystem computes the **feedforward** and hands it down with the target — `setVelocity(velocityRadPerSec, ffVolts)` — and the impl runs the velocity **PID**. This is loop-below for the feedback, loop-above for the feedforward, and it keeps Sim and Real in parity because both consume the same `ffVolts`. ### The sim model WPILib **`FlywheelSim`** — a motor driving a pure inertia (no gravity, no end stops). It models the one thing that matters here: **spin-up time**, the exponential approach to target ω set by the wheel's moment of inertia. A shooter sim that ignored this would let you "shoot" instantly; the real one makes you wait for the wheel. ### The contract From the 6328 AdvantageKit template (here in 5712 Hemlock, `2024-Eos/.../subsystems/flywheel/FlywheelIO.java`): ```java public interface FlywheelIO { @AutoLog public static class FlywheelIOInputs { /* velocityRadPerSec, appliedVolts, currentAmps */ } public default void updateInputs(FlywheelIOInputs inputs) {} public default void setVoltage(double volts) {} // open loop public default void setVelocity(double velocityRadPerSec, double ffVolts) {} // closed loop public default void stop() {} public default void configurePID(double kP, double kI, double kD) {} } ``` `setVoltage` exists for open-loop spin-up and characterization; `setVelocity` carries the closed-loop target plus the subsystem-supplied feedforward; `configurePID` pushes gains down because the velocity loop runs on the motor. The inputs struct carries the controlled variable: ```java @AutoLog public static class FlywheelIOInputs { public double positionRad = 0.0; public double velocityRadPerSec = 0.0; // ◀ the controlled variable public double appliedVolts = 0.0; public double[] currentAmps = new double[] {}; } ``` The contract omits the motor type, "is a note loaded," and any pivot or hood angle — that is a separate rotational subsystem. ### The sim implementation The sim models spin-up by running the same velocity PID the real motor would, from `FlywheelIOSim.java`: ```java public class FlywheelIOSim implements FlywheelIO { private FlywheelSim sim = new FlywheelSim( LinearSystemId.createFlywheelSystem(DCMotor.getNEO(1), 0.004, 1.5), // MOI, gearing DCMotor.getNEO(1)); private PIDController pid = new PIDController(0.0, 0.0, 0.0); private boolean closedLoop = false; private double ffVolts = 0.0; private double appliedVolts = 0.0; @Override public void updateInputs(FlywheelIOInputs inputs) { if (closedLoop) { appliedVolts = MathUtil.clamp(pid.calculate(sim.getAngularVelocityRadPerSec()) + ffVolts, -12, 12); sim.setInputVoltage(appliedVolts); } sim.update(0.02); // advance the inertia one tick inputs.velocityRadPerSec = sim.getAngularVelocityRadPerSec(); inputs.appliedVolts = appliedVolts; } @Override public void setVelocity(double velocityRadPerSec, double ffVolts) { closedLoop = true; pid.setSetpoint(velocityRadPerSec); this.ffVolts = ffVolts; } // setVoltage(), stop(), configurePID() ... } ``` The sim is fed the same `ffVolts` the subsystem computes, so a shooter that "gets to speed" in sim gets to speed on the robot. (5712's 2024 source builds the sim with the pre-2025 `FlywheelSim(gearbox, gearing, MOI)` constructor, which WPILib 2025 removed; the `LinearSystemId.createFlywheelSystem` form above is the current equivalent.) ### The subsystem The subsystem owns the feedforward and treats sim as a separate robot with its own tuning, from `Flywheel.java`: ```java public class Flywheel extends SubsystemBase { private final FlywheelIO io; private final FlywheelIOInputsAutoLogged inputs = new FlywheelIOInputsAutoLogged(); private final SimpleMotorFeedforward ffModel; public Flywheel(FlywheelIO io) { this.io = io; switch (Constants.currentMode) { // ◀ different feedforward gains per mode case REAL, REPLAY -> { ffModel = new SimpleMotorFeedforward(0.1, 0.05); io.configurePID(1.0,0,0); } case SIM -> { ffModel = new SimpleMotorFeedforward(0.0, 0.03); io.configurePID(0.5,0,0); } default -> ffModel = new SimpleMotorFeedforward(0.0, 0.0); // the final field must be assigned on every path } } @Override public void periodic() { io.updateInputs(inputs); Logger.processInputs("Flywheel", inputs); } public void runVelocity(double velocityRPM) { var radPerSec = Units.rotationsPerMinuteToRadiansPerSecond(velocityRPM); io.setVelocity(radPerSec, ffModel.calculate(radPerSec)); // subsystem owns the feedforward } @AutoLogOutput public double getVelocityRPM() { return Units.radiansPerSecondToRotationsPerMinute(inputs.velocityRadPerSec); } } ``` The source comment puts it well: the physics simulator is treated as a separate robot with different tuning. Sim and Real are two implementations behind one contract, each with its own constants, both exercised by the same code. ### Variations | Variation | Team | How it differs | |---|---|---| | Shooter = flywheel + hood/feeder | 2637 | `ShooterIO` for the wheels, a separate rotational `PivotIO` / hood, a `FeederIO` roller; a `Shooting` command coordinates them | | Plain-getter velocity | 1155 | no inputs struct; `ShooterIO` exposes `setVoltage` + `velocity()`, subsystem runs the PID above the line | | Dual-wheel / spin | many | top and bottom (or left and right) wheels at different speeds for backspin — two `setVelocity` targets | The recurring design point: a "shooter" is usually **three** archetypes — a velocity flywheel (this section), a rotational hood or pivot, and a roller feeder. Keep them as separate subsystems behind separate IO contracts and let a command compose them; don't fuse them into one class. ### The testability point The velocity test asserts the wheel reaches and holds a commanded speed within tolerance — the RPM analog of the elevator reaching a height. SciBorgs' shooting test, abridged from `ShootingTest.java`: ```java @BeforeEach public void setup() { setupTests(); shooter = Shooter.create(); pivot = Pivot.create(); feeder = Feeder.create(); drive = Drive.create(); shooting = new Shooting(shooter, pivot, feeder, drive); // ◀ whole flow, all on sim } @ParameterizedTest @ValueSource(doubles = {200, 300, 350, 400, 500, 540}) public void shootSysCheck(double v) { runUnitTest(shooter.goToTest(RadiansPerSecond.of(v))); } @ParameterizedTest @ValueSource(doubles = {-200, -100, 0, 100, 200}) public void testShootStoredNote(double vel) { run(shooting.shoot(RadiansPerSecond.of(vel))); fastForward(); assertEquals(vel, shooter.rotationalVelocity(), VELOCITY_TOLERANCE.in(RadiansPerSecond)); // ◀ at speed? } ``` `shootSysCheck` is the per-subsystem velocity check. `testShootStoredNote` is the larger prize: it constructs **four subsystems on sim and the command that coordinates them**, then asserts the end state. Because every subsystem mocks its hardware below, a whole behavior is testable with no robot — which works only if none of the four imports another. The composition lives in the `Shooting` command, not inside the shooter. That is the library rule paying off across subsystems. The vendor leak to watch here: teams reach for a `TalonFX` velocity API in the subsystem to "just set the RPM." Keep the subsystem in rad/s and `ffVolts`; let the `*IOTalonFX` impl translate to a `VelocityVoltage` request. --- ## Roller / game-piece — the actuator is trivial, the sensor is the point A roller subsystem moves a game piece *through* the robot: an **intake** pulls it in, an **indexer** or **feeder** moves it to the shooter, a **manipulator** or **claw** holds or ejects it. The actuation is trivial — run a wheel at a voltage. The defining part is the **sensor that answers "do we have it?"** — a beam-break, a distance sensor, or motor-stall current. A roller subsystem is less about control and more about detecting state transitions: empty → holding → ejected. It is the most common subsystem family in the corpus (18 teams with a clean `IntakeIO` + sim). ### The control decision Actuation is open-loop **voltage** (`run(+6V)` / `stop`) or a simple velocity; a `DCMotorSim` suffices, with no position to hold and no gravity. The **state** is the game-piece sensor, and the subsystem's real job is to expose it (`hasGamePiece()`) so commands can do "intake **until** we have a piece, then stop." Two sub-shapes: - **Fixed roller** (indexer, feeder, fixed intake): one motor plus a sensor. Pure this archetype. - **Deploying intake**: a roller plus a pivot that lowers and raises it — this archetype fused with the rotational one. The IO carries both `setRollerVoltage` and `setPivotPosition`. Keep them in one IO only if they are one mechanism. ### The sim model WPILib **`DCMotorSim`** for the roller (and pivot). But the model that *matters* is the **sensor**: to test "intake until we have a piece," the sim impl must be able to report the beam-break as tripped on command. Most teams skip this, which is why rollers are the least-tested archetype. ### The contract 6328 noticed every roller is the same and extracted a base, so the intake is a one-line tag, from `RobotCode2024Public/.../subsystems/rollers/intake/IntakeIO.java`: ```java public interface IntakeIO extends GenericRollerSystemIO {} ``` `GenericRollerSystemIO` carries `updateInputs` + `runVolts` + `stop`; `Intake`, `Feeder`, and `Indexer` are all `GenericRollerSystem`s differing only by constants. This is "generalize after the third copy" applied to the most-repeated subsystem. The richer case carries both archetypes plus the sensor, from 3476 Code Orange, `Godzilla-ReefScapeOffseason/.../subsystems/intake/IntakeIO.java`: ```java public interface IntakeIO { @AutoLog class IntakeIOInputs { public PivotData pivotData; // the deploy joint (a rotational mechanism) public RollerData rollerData; // the wheel public CanRangeData canRangeData; // the game-piece sensor } default void setPivotVoltage(double voltage) {} default void setRollerVoltage(double voltage) {} default void setPivotPosition(double positionRotations) {} default boolean checkRollerStalled() { return false; } // backup "have piece" via current } ``` `setPivotPosition` is the rotational mechanism riding alongside the roller. The sensor crosses the IO line as a plain input. 3476 models it as a CANRange distance sensor with a `tripped` flag, plus a stall check as backup: ```java record RollerData(boolean isMotorConnected, double voltage, double supplyCurrent, double statorCurrent, double temperature, double velocityRPS) {} record CanRangeData(boolean isSensorConnected, boolean tripped, // ◀ "do we have a piece?" double signalStrength, double distanceMeters) {} ``` Other teams use a `boolean beamBroken` (digital beam-break) or `checkRollerStalled()` (a current spike when a piece jams the wheel). Whatever the hardware, the sensor crosses the line as a boolean or double input, so sim can fake it. The contract omits the motor type, "should we be intaking right now," and the shooter. ### The sim implementation `DCMotorSim` for the physics, with a vendor nuance worth noting, from 3476's `IntakeIOSim.java`: ```java import com.ctre.phoenix6.sim.TalonFXSimState; // ◀ a vendor type — allowed, this is BELOW the line import edu.wpi.first.wpilibj.simulation.DCMotorSim; public class IntakeIOSim extends IntakeIOReal { protected DCMotorSim rollerSim, pivotSim; // a Notifier ticks the DCMotorSim at 200 Hz; the TalonFXSimState feeds applied voltage back in // so the *real* TalonFX control code runs against simulated physics. } ``` The nuance: this sim impl imports `com.ctre.phoenix6.sim`, and that is fine. Both `IOReal` and `IOSim` are *below* the line, so a vendor type is allowed in either. 3476 uses CTRE's sim-state (the controller simulates itself, high fidelity, tied to CTRE); SciBorgs keeps `SimElevator` pure WPILib (more portable). Both are valid — the rule is "no vendor *above* the line," not "no vendor in sim." ### Variations | Variation | Team | How it differs | |---|---|---| | Generic roller base | 6328 | one `GenericRollerSystemIO` shared by intake / feeder / indexer | | Roller + pivot + CANRange | 3476 | deploying intake; distance sensor `tripped` is the have-piece flag | | Stall-current detection | 3476, many | no beam-break — `checkRollerStalled()` infers a held piece from current | | Indexer / Feeder | 2637, 1155 | fixed roller + a beam-break; no pivot — the purest form of this archetype | | Manipulator / Claw | 3636, 3061 | a roller or a servo-gripper that holds; "have piece" via a sensor or position | ### The testability point This archetype teaches an uncomfortable truth. SciBorgs — the corpus testing leader — tests its elevator and shooter with real behavior assertions, but its `IntakeTest` is only a construction smoke-test: ```java public class IntakeTest { @Test public void init() throws Exception { Intake.create().close(); // ◀ a construction smoke-test, no behavior asserted reset(); } } ``` Why? Because the interesting behavior — "run the roller until the beam-break trips, then stop" — needs the sensor simulated, and faking a beam-break is more work than wrapping a `DCMotorSim`. So teams test that the subsystem constructs and stop there. The fix is small and high-value: make the game-piece sensor a plain IO input (a `boolean beamBroken` in the inputs struct) and give `IntakeIOSim` a way to set it. Then the real test becomes possible: ```java // the test this archetype should have: var intake = new Intake(simIO); run(intake.intakeUntilHeld()); // command runs the roller simIO.setBeamBroken(true); // fake the piece arriving fastForward(); assert intake.hasGamePiece(); // and assert the command reacted ``` That is the whole payoff of putting the sensor behind the IO line instead of reading a `DigitalInput` directly in the subsystem. The leak to watch: reading a WPILib `DigitalInput` beam-break in the subsystem is fine, but reading a CTRE CANrange or a SparkMax limit switch in the subsystem is a vendor leak — route it through the inputs struct. --- ## Reading the four together The four archetypes are one seam drawn for four control types. The contract changes only in what crosses the line and which WPILib model backs the fake hardware: | Archetype | Controlled variable | Feedforward | Sim model | The testability hinge | |---|---|---|---|---| | Linear position | height (m) | `ElevatorFeedforward` (constant `kG`) | `ElevatorSim` | command a height, assert it arrives | | Rotational position | angle (rad) | `ArmFeedforward` (`cos θ`) | `SingleJointedArmSim` | same shape; depends on a measured MOI | | Velocity | speed (rad/s) | `SimpleMotorFeedforward` (`kV`) | `FlywheelSim` | reaches and holds ω within tolerance | | Roller / game-piece | open-loop voltage | none | `DCMotorSim` + faked sensor | react to a simulated sensor trip | The decisions repeat too. Where does the loop live — above the line (Sim and Real share one controller, recommended for anything you test) or below (exploit firmware control, accept drift)? What does the sensor cross the line as (a getter, an inputs-struct field)? And in every case: the vendor type stays in the `*IO` or `*IOSim` file, never above it. Read a team's code and the archetype tells you what to expect: an `ElevatorSim` in the sim impl, an absolute encoder fused below the line in an arm, a `setVelocity(ω, ffVolts)` hybrid for a flywheel, a `boolean tripped` input on an intake. When those are present and confined, the subsystem can be tested in isolation and lifted out as a library. When the vendor type or a sibling import has crept above the line, it cannot — and that is the marker the rubric rewards. --- Next: [19. The drivetrain subsystem](19-the-drivetrain-subsystem.md) — the special case with two interfaces (module and gyro), swerve kinematics, and a sim model of its own. # 19. The drivetrain subsystem https://frc-code-scout.jointheleague.org/part-2/19-the-drivetrain-subsystem/ *The drivetrain is the one subsystem that breaks the single-IO rule. It is built from two interfaces (`ModuleIO` ×4 plus `GyroIO` ×1), a module is two control loops behind one contract, and it carries the only real math that belongs above the IO line. This chapter is the deep dive that [Part I, chapter 6](../part-1/06-the-drivetrain.md) deferred: where chapter 6 made the empirical case that the drivetrain is both an actuator and the robot's primary sensor, here we build it.* [Part I, chapter 6](../part-1/06-the-drivetrain.md) established the facts. The drivetrain is the only subsystem that is both an actuator and the robot's primary sensor; its most-consumed output is `Pose`, read 682 times across the 684-repo full clone — more than any actuator field on any subsystem. Of the ~49 classified swerve drivetrains in the 55-repo season index, about 63% stand on CTRE's generated drivetrain in some form (48% as-is, 14% wrapped), and only about 27% own a real IO seam. This chapter takes those facts as given and shows how the seam is built — at both granularities teams cut it. The drivetrain is the load-bearing example for everything earlier in Part II: the IO seam ([chapter 16](16-hardware-abstraction.md)), the motor interface that a module composes ([chapter 17](17-motor-interfaces.md)), and the velocity and position archetypes a module fuses into one contract ([chapter 18](18-subsystem-archetypes.md)). It is also the bridge to [the world model](20-the-world-model.md): the pose it estimates is the value the rest of the robot reads. --- ## 19.1 Two control problems, four times, plus kinematics A swerve drivetrain moves and rotates the robot by independently steering and driving four corner modules, and it maintains odometry — where am I, computed from wheel motion plus gyro. It is the most complex subsystem on the robot, but it is the same IO pattern scaled up. The `Drive` subsystem holds four `ModuleIO`s and one `GyroIO`, runs kinematics above them, and never names a motor. Three pieces of logic live in or around the subsystem: - A **module** is a velocity loop (the drive wheel, the velocity archetype) *and* a position loop (the turn/azimuth, the rotational-position archetype) behind one `ModuleIO`. Two control problems, one contract, four copies. - **Kinematics** (`SwerveDriveKinematics`) lives in the `Drive` subsystem, above the line. It converts a robot-level `ChassisSpeeds` into four `SwerveModuleState`s and back. This is the one subsystem with real math above the IO line, and that is correct — kinematics is vendor-free geometry. - **Odometry**: each module reports its drive distance and turn angle, the gyro reports yaw, and a `SwerveDrivePoseEstimator` fuses them into a pose. Vision then corrects it ([chapter 21](21-vision-systems.md)). ```d2 Drive: { shape: class -modules: "ModuleIO[4]" -gyro: GyroIO -kinematics: SwerveDriveKinematics -estimator: SwerveDrivePoseEstimator +drive(ChassisSpeeds) +pose(): Pose2d } ModuleIO: { shape: class +updateInputs(ModuleIOInputs) +runDriveVelocitySetpoint(v, ff) +runTurnPositionSetpoint(angle) } GyroIO: { shape: class +updateInputs(GyroIOInputs) } ModuleIOTalonFX: { shape: class } ModuleIOSim: { shape: class } GyroIOPigeon2: { shape: class } GyroIOSim: { shape: class } Drive -> ModuleIO: "holds (4)" Drive -> GyroIO: holds ModuleIOTalonFX -> ModuleIO: implements ModuleIOSim -> ModuleIO: implements GyroIOPigeon2 -> GyroIO: implements GyroIOSim -> GyroIO: implements ``` ### High-frequency odometry Good drivetrains sample module positions and gyro yaw at 250 Hz, not the 50 Hz robot loop, so odometry does not smear during fast motion. That is why the inputs carry *arrays* — `odometryDrivePositionsMeters[]`, `odometryTurnPositions[]`, `odometryYawPositions[]` — a batch of timestamped samples per cycle, not a single reading. This is the swerve-specific detail that distinguishes a serious drivetrain from a working one. The thread only produces the samples; the loop that consumes them is shown in §19.2. --- ## 19.2 The per-module seam — `ModuleIO` + `GyroIO` When you build the drivetrain from motors, the seam is cut per module. Each module gets one `ModuleIO`; the heading sensor gets its own `GyroIO`. This is the AdvantageKit template, used by the six per-module teams in the 55-repo season index (6328 and 2637 among them). ### `ModuleIO` — drive (velocity) + turn (position) in one interface | Method | Crosses as | Archetype | |---|---|---| | `runDriveVelocitySetpoint(v, ff)` | command | velocity | | `runTurnPositionSetpoint(angleRads)` | command | rotational position | | `runDriveVolts` / `runCharacterization` | command | open-loop / characterization | | `setDrivePID` / `setTurnPID` | config | on-motor loops | | `updateInputs(inputs)` | input | positions, velocities, odometry sample arrays | The 6328 AdvantageKit drive is the most-forked swerve base in FRC and the reference for the per-module pattern: *6328 Mechanical Advantage — `RobotCode2024Public/.../subsystems/drive/ModuleIO.java`* ```java public interface ModuleIO { @AutoLog class ModuleIOInputs { public double drivePositionRads, driveVelocityRadsPerSec, driveAppliedVolts; public Rotation2d turnAbsolutePosition, turnPosition; // azimuth (rotational) public double turnVelocityRadsPerSec; public double[] odometryDrivePositionsMeters = new double[] {}; // 250 Hz batches public Rotation2d[] odometryTurnPositions = new Rotation2d[] {}; } default void updateInputs(ModuleIOInputs inputs) {} default void runDriveVelocitySetpoint(double velocityRadsPerSec, double feedForward) {} // velocity loop default void runTurnPositionSetpoint(double angleRads) {} // position loop default void setDrivePID(double kP, double kI, double kD) {} default void setTurnPID(double kP, double kI, double kD) {} default void setDriveBrakeMode(boolean enable) {} default void stop() {} } ``` One interface, two control problems. `runDriveVelocitySetpoint` is the velocity command; `runTurnPositionSetpoint` is the position command. The inputs struct carries both single-cycle readings and the 250 Hz odometry arrays. ### `GyroIO` — the second, tiny interface A heading sensor is not a module, so it gets its own interface. It is the smallest IO in the codebase — yaw and odometry yaw samples, no actuation. Like vision, the gyro is sensor-only. *6328 Mechanical Advantage — `RobotCode2024Public/.../subsystems/drive/GyroIO.java`* ```java public interface GyroIO { @AutoLog class GyroIOInputs { public boolean connected = false; public Rotation2d yawPosition = new Rotation2d(); public Rotation2d[] odometryYawPositions = new Rotation2d[] {}; public double yawVelocityRadPerSec = 0.0; } default void updateInputs(GyroIOInputs inputs) {} } ``` `ModuleIOTalonFX` and `GyroIOPigeon2` are the device implementations, where the vendor SDK is confined. `ModuleIOSim` and `GyroIOSim` wrap `DCMotorSim`s (or maple-sim). Neither interface names a `TalonFX`, `Pigeon2`, or `CANcoder`; neither carries kinematics — that is the subsystem's job — and neither carries field or auto logic. ### The subsystem takes its IO by constructor The `Drive` subsystem takes its IOs by constructor — the cleanest expression of the seam. From SciBorgs' construction: ```java drive = new Drive(gyro, frontLeft, frontRight, rearLeft, rearRight); // 1 GyroIO + 4 ModuleIO ``` `Drive` owns the `SwerveDriveKinematics`, the `SwerveDrivePoseEstimator`, and the 250 Hz odometry thread. It converts `ChassisSpeeds` to module setpoints and reads odometry back — all above five IO seams, none of which name a vendor. The consuming side of the 250 Hz thread from §19.1 lives in `Drive.periodic()`. The thread only fills the sample arrays; each 20 ms loop drains every batched sample into the pose estimator in timestamp order, so the estimate integrates at 250 Hz even though the robot loop runs at 50: ```java // Drive.periodic(), abridged — drain the 250 Hz batches into the estimator for (int i = 0; i < sampleTimestamps.length; i++) { // several samples per 20 ms loop for (int m = 0; m < 4; m++) positions[m] = new SwerveModulePosition( moduleInputs[m].odometryDrivePositionsMeters[i], moduleInputs[m].odometryTurnPositions[i]); poseEstimator.updateWithTime( sampleTimestamps[i], gyroInputs.odometryYawPositions[i], positions); } ``` --- ## 19.3 Two granularities of the seam The per-module template is not the only place to cut. The corpus shows there are two granularities at which teams cut the IO seam, and the right one depends on what is below it. | Granularity | seam | who | when it's right | |---|---|---|---| | **Per-module** | `ModuleIO` + `GyroIO` (one drive + steer + encoder ×4) | AdvantageKit template, the 6 per-module teams | you **build from motors** | | **Per-drivetrain** | one `DriveIO`/`SwerveIO` wrapping the whole CTRE swerve | 254, 2910 | you **wrap a vendor's swerve** | Both are the same seam — data-struct on the read side, intent on the write side, vendor types below only — cut at different heights. The deciding factor is what is below the line. If you build from motors, there is no swerve abstraction yet, so you cut per module. If you wrap CTRE's swerve, the vendor has already abstracted the modules for you; re-cutting per-module below `CommandSwerveDrivetrain` would be redundant. So 254 and 2910 cut once, around the whole thing. The coarse, per-drivetrain contract is tiny: **`SwerveRequest` in, `SwerveDriveState` out.** That is the whole surface, and the rest of this chapter is about that surface and how elite teams keep `com.ctre` behind it. --- ## 19.4 The CTRE `CommandSwerveDrivetrain` device surface About 63% of teams stand on CTRE's generated drivetrain, so its surface is the de-facto drive API. What it is: ```java class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Subsystem // TunerSwerveDrivetrain extends SwerveDrivetrain ``` CTRE's generic `SwerveDrivetrain` plus the one `implements Subsystem` that makes it command-schedulable. The generated subclass adds a tight surface. These are the consensus methods, by team prevalence, across the 19 of the 55 season repos that define a class named `CommandSwerveDrivetrain`. (Nineteen is a class-name count, so it undershoots the ~31 of 49 swerve repos — the 63% above — that stand on the generator in some form: renamed and customized copies escape it.) | Method | teams | role | |---|---|---| | `applyRequest(Supplier)` | 16 | the control entry point (`= run(() -> setControl(req))`) | | `startSimThread()` | 15 | 5 ms `Notifier` → `updateSimState(dt, V)` | | `periodic()` | 12 | applies alliance/operator perspective | | `addVisionMeasurement(...)` | 11 | pose fusion (often overridden for `fpgaToCurrentTime`) | | `sysIdQuasistatic` / `sysIdDynamic` | 10 | characterization | | `configureAutoBuilder` | 6 | PathPlanner wiring | Everything substantive — `setControl`, `getState`, `registerTelemetry`, `resetPose`, `setOperatorPerspectiveForward` — is inherited from CTRE's `SwerveDrivetrain`. Two leaks force a wrap if you want vendor-neutrality: the class *is* a `SwerveDrivetrain`, and `getModule(i)` / `getPigeon2()` return raw `TalonFX` / `Pigeon2`. Strip it to the contract and it is exactly the coarse `DriveIO` of §19.3 — request in, state out. ### The two contract types The two CTRE types that *are* the drive contract, ranked by real corpus usage. `SwerveRequest` is the control vocabulary — intent objects, not method calls: ```java interface SwerveRequest { StatusCode apply(SwerveControlParameters params, SwerveModule... modules); } ``` | Request (by corpus uses) | uses | purpose | |---|---|---| | `FieldCentric` | 324 | field-relative translate + rotate (default teleop) | | `SysIdSwerveRotation` | 129 | rotation characterization | | `SwerveDriveBrake` | 119 | X-lock the wheels | | `FieldCentricFacingAngle` | 115 | field translate, heading held by a θ-PID | | `ApplyRobotSpeeds` (+ `ApplyFieldSpeeds` 25) | 102 | raw `ChassisSpeeds` (path-follower entry) | | `SysIdSwerveTranslation` / `SysIdSwerveSteerGains` | 92 / 91 | translation / steer characterization | | `RobotCentric` | 80 | robot-relative drive | | `PointWheelsAt` | 74 | aim modules at one angle | | `Idle` | 48 | do nothing | Builder usage confirms the modifier surface: `withVelocityX/Y` (368 / 353), `withDriveRequestType` (337, the open-vs-closed-loop switch), `withRotationalRate` (294), `withDeadband` / `withRotationalDeadband` (148 / 126), `withTargetDirection` (101, for FacingAngle), `withForwardPerspective` (58), and `withWheelForceFeedforwardsX/Y` (44 / 40) — the channel by which a setpoint generator's per-module forces reach the wheels. `SwerveDriveState` is the output, modeled as a flat, PascalCase, public-field POD. Field-access counts: | Field | accesses | | Field | accesses | |---|---|---|---|---| | `Pose` | **682** | | `ModulePositions` | 55 | | `ModuleStates` | 322 | | `FailedDaqs` / `SuccessfulDaqs` | 19 / 17 | | `Speeds` | 243 | | `RawHeading` | 17 | | `ModuleTargets` | 154 | | `Timestamp` | ~0 | | `OdometryPeriod` | 101 | | | | `Pose` is read 682 times — more than any actuator field on any subsystem in the corpus. That number is the proof that the drivetrain's most important output is not its motion command but its state estimate, *where am I*, because auto, aiming, and vision all consume it. CTRE shipped this state as a flat data struct — the same inputs-struct-as-data idea AdvantageKit formalizes with `@AutoLog`, arrived at independently. Modeling a neutral `DriveIOInputs` on these fields adopts a convergent consensus rather than inventing one. --- ## 19.5 Demoting CTRE below the line — the 254/2910 pattern The clearest evidence of the elite pattern is what the strongest teams' `drive/` directories contain. Two examples, verbatim from disk: ``` 254 (2025) subsystems/drive/ 2910 (2026) subsystems/drive/ DriveSubsystem.java <- Subsystem SwerveSubsystem.java <- Subsystem DriveIO.java @AutoLog <- IO SEAM SwerveIO.java @AutoLog <- IO SEAM (two inputs structs) DriveIOHardware.java <- real impl SwerveIOCTRE.java <- real impl DriveIOSim.java <- sim impl SwerveIOSim.java <- sim impl CommandSwerveDrivetrain.java <- CTRE CommandSwerveDrivetrain.java <- CTRE device Comp/Prac/SimTunerConstants <- owned SetModulePositionsRequest.java <- custom SwerveRequest DriveViz.java <- telemetry ``` Three things this layout proves: 1. **The three roles are split into separate files.** `DriveSubsystem` (the command target) sits on top of `DriveIO` (the seam to the device), with the pose estimator inside the subsystem. 2. **The generated `CommandSwerveDrivetrain` is demoted to a device.** In both repos it does *not* `implements Subsystem` — it is a plain class wrapped by `DriveIOHardware` / `SwerveIOCTRE` so `com.ctre` stops at the seam and never reaches the subsystem. Contrast the 48% who let the generated class be their subsystem directly. 3. **They ingest the generator's constants but own the architecture.** 254 keeps three `TunerConstants` variants (`Comp` / `Prac` / `Sim`) as owned files: generate the numbers, own the architecture. The hand-rolled end of the spectrum reaches for a seam in spirit too. Team 868's Ri3D `Drivetrain extends SubsystemBase implements BaseSwerveDrive` — a team-defined `BaseSwerveDrive` interface — shows even a one-week robot wants a contract in front of the hardware. The vendor rule, stated plainly: > **Banned above the line:** `com.ctre.*`, `com.revrobotics.*`, the Pigeon/NavX SDK. They live in `ModuleIOTalonFX` / `GyroIOPigeon2`, or in `DriveIOHardware` / `SwerveIOCTRE` when wrapping a vendor swerve. Allowed above: WPILib `kinematics`, `geometry`, `SwerveDrivePoseEstimator`. The honest exception: CTRE's Tuner X generator produces a `SwerveDrivetrain` you call directly, and YAGSL is a swerve library with its own internal seam. Both put a vendor or library type above *your* IO line. That is a deliberate trade — you give up the clean seam (and the simple sim and test below) in exchange for a configured drivetrain in an afternoon. If you take it, keep the generated drivetrain behind your own thin `DriveIO`-style wrapper so the rest of the robot still does not import `com.ctre`. Otherwise a swerve-vendor change becomes a robot-wide refactor. Teams that hand-roll `ModuleIO` (6328) keep the seam — as does 254, whose 2025 code wraps the generated swerve behind a per-drivetrain `DriveIO` instead; teams that generate and stop there trade it away knowingly. --- ## 19.6 The optional `SwerveSetpointGenerator` The `withWheelForceFeedforwardsX/Y` builders above hint at a third tier of control. A `SwerveSetpointGenerator` sits between the desired `ChassisSpeeds` and the module setpoints, enforcing module slew limits and traction constraints so the drivetrain does not command states the wheels cannot achieve. It produces per-module force feedforwards that ride into the request through those `withWheelForceFeedforwards` channels. This is elite-practice tail, not baseline. `SwerveSetpointGenerator` appears in 9 teams and maple-sim in 11 — the same small set that owns the seam. It is optional; the contract works without it. But when present it lives above the IO line, like kinematics, because it is constraint math over geometry, not a vendor call. --- ## 19.7 Mock below, test above The swerve test is the richest mock-below example in the corpus. Construct `Drive` from four `SimModule`s and a `NoGyro`, command velocities, and assert both the chassis speeds *and* the odometry pose: *1155 SciBorgs — `Reefscape-2025/src/test/java/.../robot/SwerveTest.java`* ```java @BeforeEach public void setup() { setupTests(); drive = new Drive(new NoGyro(), new SimModule("FL"), new SimModule("FR"), new SimModule("RL"), new SimModule("RR")); // 5 IO mocks } @RepeatedTest(5) public void reachesRobotVelocity() { double vx = rand(), vy = rand(); run(drive.drive(() -> vx, () -> vy, () -> Rotation2d.kZero, () -> 0)); fastForward(500); ChassisSpeeds s = drive.fieldRelativeChassisSpeeds(); assertEquals(vx, s.vxMetersPerSecond, DELTA); // kinematics correct assertEquals(vy, s.vyMetersPerSecond, DELTA); } @RepeatedTest(5) public void testModuleDistance() { // command a field-relative velocity for deltaT seconds... run(c); fastForward(Seconds.of(deltaT)); Pose2d pose = drive.pose(); assertEquals(deltaX, pose.getX(), DELTA * 4); // odometry integrates correctly assertEquals(deltaY, pose.getY(), DELTA * 4); } ``` This single test exercises kinematics *and* odometry with no hardware. `reachesRobotVelocity` checks the `ChassisSpeeds`-to-module-state math; `testModuleDistance` drives for a fixed time and asserts the pose moved the integrated distance. SciBorgs leaves `systemCheck` and the align-to-pose test `@Disabled` — honest about which parts the sim is tuned for. It works only because `Drive` takes its five IOs by constructor; swap in real `ModuleIOTalonFX` / `GyroIOPigeon2` and the same `Drive` runs the robot. Sim has two levels. Per-module `DCMotorSim` simulates each motor independently — enough to test kinematics and odometry. maple-sim runs whole-drivetrain rigid-body physics including wheel slip and collisions — the rung where sim can surprise you. The 11 teams with maple-sim back `ModuleIOSim` with whole-drivetrain dynamics rather than four independent motors. --- ## 19.8 Rip it out as a library The drivetrain is the subsystem teams most successfully extract. `3061-lib`, YAGSL, and CTRE's generator are all "swerve as a dependency." The `drive/` package needs WPILib geometry and kinematics plus its IO interfaces; its only outward coupling is feeding pose to `RobotState`, or owning the estimator itself. It must not import `Arm`, `Intake`, or `Superstructure`. That it is the most commonly published-as-a-library subsystem is proof the seam holds when you keep kinematics vendor-free and the motors (or the whole vendor swerve) behind an IO. The seam at either granularity buys the same thing: a drivetrain you can test in sim, swap vendors under, and lift into the next season's robot. --- ## 19.9 Checklist — is your drivetrain intact? - [ ] A seam cut at the right granularity: per-module `ModuleIO` + `GyroIO` if you build from motors, a single per-drivetrain `DriveIO` / `SwerveIO` if you wrap a vendor swerve. - [ ] `ModuleIO` carries both `runDriveVelocitySetpoint` (velocity) and `runTurnPositionSetpoint` (position), plus odometry sample arrays for high-frequency integration. - [ ] A separate, sensor-only `GyroIO` (yaw + odometry yaw). - [ ] `Drive` takes its IO by constructor; kinematics and pose estimator live in the subsystem, vendor-free. - [ ] If you generate the drivetrain (Tuner X / YAGSL), `CommandSwerveDrivetrain` does not `implements Subsystem` — it is demoted behind your own IO so `com.ctre` stops at the line. - [ ] A test builds `Drive` from sim modules + a no-op gyro and asserts chassis speeds *and* odometry pose. - [ ] (Stretch) `ModuleIOSim` uses maple-sim for whole-drivetrain physics; a `SwerveSetpointGenerator` enforces traction limits above the line. --- The drivetrain owns the pose, but it does not own the *world*. Pose is one input to a larger estimate that fuses vision, game-piece tracking, and field state. The next chapter follows that pose off the drivetrain and into [the world model](20-the-world-model.md). # 20. The world model https://frc-code-scout.jointheleague.org/part-2/20-the-world-model/ *Part I argued for centralizing the robot's pose estimate into one shared object; it deferred the mechanics. This chapter shows them: RobotState as a subsystem with no hardware, the two input streams it reconciles, the time-interpolating buffer that rewinds and replays them, and the unit test that makes it the easiest class on the robot to verify.* [Part I chapter 4](../part-1/04-the-state-seam.md) made the case for the state seam: one fused estimate that vision, pathing, and auto all share, instead of each subsystem keeping its own guess. That chapter stopped at the boundary. This one crosses it — how observations come in, how a delayed vision frame gets reconciled against fast odometry, and what the class is and is not allowed to import. ## A subsystem with no hardware Every other subsystem in this half of the book owns a mechanism: a drivetrain ([19](19-the-drivetrain-subsystem.md)), an elevator or arm ([18](18-subsystem-archetypes.md)), a camera ([21](21-vision-systems.md)). Each has an IO layer ([16](16-hardware-abstraction.md)) hiding a vendor device behind an interface ([17](17-motor-interfaces.md)), and each runs a control loop ([15](15-control-path.md)) that turns a setpoint into motor output. RobotState has none of that. Its IO is *observations in, belief out*. Sensors write to it — the drivetrain feeds wheel odometry, vision feeds AprilTag corrections — and decisions, pathing, and auto read from it. No motor ever moves. It is the purest example of the architecture's ethic: zero vendor types, zero IO implementations, just math. That same purity makes it the single most testable class on the robot. It owns the robot's best belief about the world — where it is on the field (pose), and, at the top of the ladder, its game-piece and mechanism state too. Centralizing that belief is what makes vision, pathfinding, and auto agree on "where are we." It is the difference between a pose estimate merely *existing* and a world model *being* the architecture. ## The I/O boundary — two streams, no control loop Two streams come in, at different rates and latencies: ``` Drive → addOdometryObservation(wheelPositions, gyro, t) ┐ Vision → addVisionObservation(pose, t, stdDevs) ├→ RobotState → getPose() / sampleAt(t) ┘ (everyone reads) ``` **Odometry** is fast and continuous but drifts — integrate wheel motion long enough and the estimate wanders. **Vision** is accurate but sparse and *delayed*: a camera frame is timestamped in the past, because capture, exposure, and pose solving all take time. By the moment a vision pose lands, the robot has already moved. RobotState reconciles the two with a **time-interpolating buffer**. It keeps roughly two seconds of timestamped odometry poses. When a delayed vision measurement arrives, it rewinds to where the robot *was* at that observation's timestamp, blends the correction in by a Kalman gain, and replays odometry forward to now. The "IO" here is purely informational — observations in, a fused `Pose2d` out. ```d2 direction: right DR: "Drive subsystem" VIS: "Vision subsystem" RS: "RobotState pose estimator + time buffer" AUTO: "Auto / Path / Aim / Superstructure" DR -> RS: "odometry (fast)" VIS -> RS: "vision obs (delayed)" RS -> AUTO: "getPose()" RS -> VIS: "sampleAt(t)" ``` The diagram has an arrow most readers skip: `sampleAt(t)` runs *back* toward Vision. Vision asks RobotState what the pose was at a past instant so it can latency-correct its own measurement before handing it back. The buffer is shared infrastructure, not a one-way sink. ## The contract ### The API | Method | Direction | Why | |---|---|---| | `addOdometryObservation(obs)` | in (write) | integrate wheel motion + gyro into the estimate, buffer it | | `addVisionObservation(obs)` | in (write) | blend a timestamped AprilTag pose by its std-dev weight | | `getPose()` / `getEstimatedPose()` | out (read) | the fused belief — what auto and aiming use | | `sampleAt(timestamp)` | out (read) | the pose at a past instant (for latency-correcting a measurement) | | `resetPose(pose)` | in | seed the estimate (auto start, operator reset) | Five methods. Two write, two read, one seeds. Everything a consumer needs and nothing it does not. ### What the contract omits There is no `TalonFX`, no `PhotonCamera`. There is no IO interface and — this is the part teams get wrong — **no `Drive` or `Vision` object reference**. RobotState does not hold the subsystems that feed it; it holds only the *observations* they hand over: a `Pose2d`, a `SwerveModulePosition[]`, a timestamp, a std-dev vector. It carries no commands and no game logic. It is pure `edu.wpi.first.math` geometry. The test for whether your contract is clean is mechanical: grep the imports. If you find a vendor package or a subsystem class, the seam has leaked. ## Real implementation — 6328's RobotState 6328 Mechanical Advantage hand-rolls its estimator rather than using WPILib's built-in one. The reason is the buffer: hand-rolling buys ownership of the replay logic, and ownership is what lets the rest of the world model grow off the same object. The file is `RobotCode2025Public/.../RobotState.java`. ### The observations in ```java public void addOdometryObservation(OdometryObservation obs) { Twist2d twist = kinematics.toTwist2d(lastWheelPositions, obs.wheelPositions()); lastWheelPositions = obs.wheelPositions(); odometryPose = odometryPose.exp(twist); obs.gyroAngle().ifPresent(g -> odometryPose = new Pose2d(odometryPose.getTranslation(), g.plus(gyroOffset))); // gyro overrides integrated heading poseBuffer.addSample(obs.timestamp(), odometryPose); // ◀ the 2-second time buffer estimatedPose = estimatedPose.exp(lastOdometryPose.log(odometryPose)); lastOdometryPose = odometryPose; // remember for the next delta } ``` Read it top to bottom. Wheel positions become a `Twist2d` (a small motion delta) through the kinematics. That twist is composed onto `odometryPose` with `.exp()`. If a gyro angle is present it overrides the integrated heading — wheels are good at distance, a gyro is better at heading. The new odometry pose is stamped into `poseBuffer` with its timestamp; that single line is the two-second history the vision blend depends on. Finally the fused `estimatedPose` advances by the same delta the raw odometry just took. Note what is being kept separate. `odometryPose` is the pure dead-reckoning track. `estimatedPose` is the fused belief. The buffer holds the odometry track so the estimate can be rewound and recomputed when a correction arrives. ### The vision blend — latency-corrected, std-dev weighted ```java public void addVisionObservation(VisionObservation obs) { // skip if older than the buffer; else get the odometry pose AT the observation's timestamp var sample = poseBuffer.getSample(obs.timestamp()); if (sample.isEmpty()) return; // the rewind: odometry accumulated since the sample, and the estimate as it stood back then Transform2d sampleToOdometryTransform = new Transform2d(sample.get(), odometryPose); Pose2d estimateAtTime = estimatedPose.plus(new Transform2d(odometryPose, sample.get())); // build a 3x3 Kalman gain by comparing obs.stdDevs() against the estimator's fixed odometry // std-devs, blend vision into the rewound estimate, then replay odometry forward to now: Matrix visionK = /* from obs.stdDevs() vs. odometry std-devs */; Transform2d correction = new Transform2d(estimateAtTime, obs.visionPose()); // scaledByK is pseudocode — scale the correction's (x, y, θ) by the gain's diagonal estimatedPose = estimateAtTime.plus(scaledByK(correction, visionK)).plus(sampleToOdometryTransform); } public record VisionObservation(Pose2d visionPose, double timestamp, Matrix stdDevs) {} ``` The first two lines are the latency correction. `poseBuffer.getSample(obs.timestamp())` retrieves where the robot was at the moment the frame was captured — not now. If the observation is older than the buffer holds, the method returns rather than guessing. The blend is three operations. `visionK` is a 3×3 Kalman gain built by comparing the observation's std-devs against the estimator's fixed odometry std-devs — trust is relative, not absolute: a measurement tighter than the odometry pulls the estimate hard toward the vision pose, a looser one barely nudges it. `correction` is the transform from the rewound estimate to the vision pose. The estimate gets the correction scaled by the gain, *then* `sampleToOdometryTransform` replays the odometry that accumulated between the observation's timestamp and now. The result is a pose that incorporated the past measurement but is current. That `record VisionObservation` is the whole input type — a pose, a timestamp, a std-dev matrix. No camera, no pipeline, no vendor handle. Vision computes those three values and hands them over. The estimator core is roughly 80 lines of `edu.wpi.first.math` — `Twist2d`, `Transform2d`, `Matrix`, `TimeInterpolatableBuffer` — though the full `RobotState` is larger, because it also carries the game-piece and mechanism state described next. Grep the imports and you will not find one vendor type. The state seam is math, not hardware. ### The world model grows off it 6328's `RobotState` does not stop at pose. It also carries `@AutoLogOutput` game-piece observations (coral and algae for the 2025 game), robot velocity, and mechanism state like `elevatorExtensionPercent` and `intakeDeployPercent`. The robot's belief about *everything* lives in one inspectable, logged place. That is the D7 L4 world model: not just where the robot is, but a shared, logged model of the whole situation. ## Who owns the estimator — the D7 climb The state seam sits on the rubric's D7 ladder; [chapter 21](21-vision-systems.md) has the full 0–4 table. What matters for this chapter is *ownership*. At D7 L2 the estimator is WPILib's `SwerveDrivePoseEstimator` — a RobotState-lite with `addVisionMeasurement`, the time buffer, and the Kalman fusion already built in — owned *privately by Drive*, so vision and auto have to reach into the drivetrain to read or correct the pose. At D7 L4 the estimator is *extracted* into a dedicated `RobotState`, decoupled from Drive: vision and auto talk to the world model, not the drivetrain, and the model has room to grow into game-piece and mechanism state. (L3, between them, is the trust gate — rejection and weighting before fusing — also chapter 21's territory.) The architectural move is the *extraction*: making one object the place pose lives. Hand-rolling the estimator (6328) is the upgrade that follows — it buys ownership of the replay buffer and the blend. Whether `RobotState` is a singleton (6328's `getInstance()`) or constructor-injected is a style choice — injected is friendlier to tests, as the next section shows. Across the corpus, a `RobotState` class — a named, dedicated world-model object rather than an estimator buried in Drive — appears in 26 of the 55 repos in the season index. ## The easiest test on the robot, and the least written ### Nothing to mock Every other subsystem test needs a mock IO below the line so the control loop has something to drive in simulation. RobotState has no IO and no loop. It takes plain data. So the test is trivial: ```java // the test RobotState makes possible (and most teams never write): // helpers (abridged): odom(t) builds an OdometryObservation at time t for a straight // 1 m/s +X drive; tightStdDevs = VecBuilder.fill(0.05, 0.05, 0.05) — a high-trust measurement var state = new RobotState(kinematics); state.resetPose(Pose2d.kZero); // feed a straight 1 m/s drive for 1 s as odometry samples... for (double t = 0; t < 1.0; t += 0.02) state.addOdometryObservation(odom(t)); assertEquals(1.0, state.getPose().getX(), 0.02); // odometry integrates state.addVisionObservation(new VisionObservation(new Pose2d(0.9,0,kZero), 0.98, tightStdDevs)); assertTrue(state.getPose().getX() < 1.0); // vision pulled it back ``` Two assertions cover the contract. The first feeds a straight one-meter drive as odometry samples and checks that the estimate integrated to roughly one meter — dead reckoning works. The second feeds a single tight vision observation reading 0.9 m at timestamp 0.98 and checks that the fused pose moved *back* toward it — the blend works. No HAL, no robot, no other subsystem. Feed observations, assert the belief. This is the finding worth dwelling on: RobotState is the most unit-testable class on the robot — pure functions of input, nothing to mock, no hardware to stand up — and almost nobody tests it. It is the highest-value test a team is not writing. Constructor injection helps here. `new RobotState(kinematics)` is a one-liner because the kinematics is passed in; a singleton that reaches into `Drive` for its kinematics cannot be built this cleanly in a test. ### Already a library RobotState imports `edu.wpi.first.math.*` and nothing else load-bearing. It is *already* a standalone, reusable estimator — you could lift it into another project unchanged. The thing that would break that property is importing a subsystem object or a vendor type. If either appears, the seam has leaked. ### Vendor discipline — trivially clean, and that is the point > A correct `RobotState` cannot leak a vendor type **because it never touches hardware.** If you see `com.ctre` or `org.photonvision` in your RobotState, something is wrong — a subsystem is handing it a device handle instead of an observation. The fix is at the caller: subsystems compute `Pose2d` / observations and pass *those*. Most chapters in this book frame vendor discipline as work — building IO interfaces, hiding device types behind them. RobotState is the one place where the discipline costs nothing, because the class has no hardware to hide. That makes it a clean signal: a vendor import in RobotState is not a style nit, it is proof a subsystem is leaking device handles across a boundary that should only ever carry data. ## Checklist — is your state seam intact? - [ ] One `RobotState` owns the pose estimate; Drive does **not** privately own the estimator. - [ ] It takes `addOdometryObservation` + `addVisionObservation(pose, t, stdDevs)` and exposes `getPose()` / `sampleAt(t)`. - [ ] A `TimeInterpolatableBuffer` reconciles delayed vision against past odometry. - [ ] Its imports are WPILib math/geometry (plus logging) only — **no vendor, no IO impl, no subsystem object**. - [ ] Vision feeds it (and only it); auto and aim read pose *from* it, never from Drive. - [ ] A unit test feeds odometry plus a vision observation and asserts the fused pose — the easy test almost no one writes. --- The world model is where every other subsystem's output converges and where the next layer's input begins. The stream that corrects it — sparse, delayed, and the hardest to trust — gets its own chapter next: [21. Vision systems](21-vision-systems.md). # 21. Vision systems https://frc-code-scout.jointheleague.org/part-2/21-vision-systems/ *Vision is the subsystem that breaks the mold: no motor, no setpoint, no control loop. The IO is a pure sensor that produces observations, and the subsystem's only job is to hand them to the world model. This chapter follows the whole path — coprocessor to observation to `VisionIO` interface to `RobotState.addVisionMeasurement` — and shows where the trust gate sits, what the system produces, and who reads each output.* [Chapter 4, the state seam](../part-1/04-the-state-seam.md), named vision as the input that fuses into `RobotState` and pointed here for the rest. This is that deep dive. Vision feeds the world model of [chapter 20](20-the-world-model.md): everything downstream — auto, pathing, aiming — reads the *fused* pose from `RobotState` and never talks to the camera. Part I argued why localization matters; this chapter shows the vision system end to end. ## What the subsystem is for A vision subsystem turns what the cameras see — AprilTags on a known field — into a correction to the robot's belief about where it is. It actuates nothing. It reads camera frames, computes a field-relative pose estimate with a timestamp and a confidence, and hands that to `RobotState`, which fuses it with wheel odometry. This is why vision is a subsystem at all: it owns the camera hardware behind an IO seam so the rest of the robot stays vendor-agnostic. That seam matters more for vision than for any other mechanism, because the vendor choice is a live, mid-season decision. PhotonVision and Limelight are competing coprocessor stacks, and teams switch between them when one performs better at an event. A clean `VisionIO` makes that switch a one-file change. A team that calls `LimelightHelpers.getBotPose()` directly inside its drivetrain has welded itself to Limelight — the exact leak the seam exists to prevent. `addVisionMeasurement` is called in **50 of 55** corpus teams. A note on the name before it recurs: WPILib's estimator calls this method `addVisionMeasurement`, while 6328's hand-rolled `RobotState` calls its equivalent `addVisionObservation` — the same seam under two names, and the corpus grep marker is `addVisionMeasurement`. Pose estimation is assumed; it is the floor, not the ceiling. The differences that separate teams are *where* pose estimation runs and *how* bad observations are rejected before they corrupt the estimate. ## The pipeline, end to end There is no control loop. The data moves one direction, from camera to world model: ``` Camera → VisionIO.updateInputs(observations) → Vision subsystem (filter by ambiguity / std-dev) → RobotState.addVisionObservation(pose, timestamp, stdDevs) → pose estimator fuses with odometry ``` The governing rule: **vision never talks to drive.** It writes a measurement to `RobotState`; drive, auto, and aiming all read pose *from* `RobotState`. Swapping PhotonVision for Limelight is a new `VisionIO` implementation and touches nothing else. ```d2 direction: right CAM: "Camera (PhotonVision / Limelight)" VIO: "VisionIO impl (vendor confined HERE)" VSUB: "Vision subsystem (reject high-ambiguity)" RS: "RobotState (pose estimator)" DRIVE: "Drive / Auto / Aim (read only)" CAM -> VIO VIO -> VSUB: "PoseObservation(pose, t, stdDevs)" VSUB -> RS: "addVisionObservation" RS -> DRIVE: "getPose()" ``` Each stage has a job. The camera and coprocessor detect tags and, in most stacks, compute a candidate pose. The `VisionIO` implementation receives that result and packages it as an observation — pose, timestamp, and the evidence needed to weight it. The subsystem applies the trust gate, rejecting or downweighting weak frames. `RobotState` blends the surviving observations into the pose estimator. The rest of the robot reads only the fused result. ### Where the pose is computed Two architectures split the corpus by where the estimation work lands: - **Estimate on the RIO.** 3061 Huskie Robotics runs a `PhotonPoseEstimator` inside the IO implementation and emits a `Pose2d` observation. Everything stays in robot code; nothing extra runs off-board. Simpler, but the RIO does the math. - **Estimate on a coprocessor.** 6328 Mechanical Advantage's "Northstar" computes the pose off-RIO; the IO just receives frames or poses over NetworkTables. Limelight MegaTag is the same shape — the coprocessor produces the pose, the IO reads it. Less RIO load, more infrastructure to maintain. Both feed the same `addVisionObservation` call. The difference is which side of the NetworkTables boundary the `PhotonPoseEstimator` work happens on, and that difference is hidden behind the IO interface. ## The `VisionIO` seam Vision is the archetype where the IO contract is most clearly *sensor-only*. The interface produces observations and accepts camera configuration. It exposes nothing that moves the robot. | Method | Crosses as | Why | |---|---|---| | `updateInputs(inputs)` | input only | fills the observation struct (poses, timestamps, tags seen, fps) | | `setPipeline(int)` / `setRecording(bool)` | config | camera configuration — not robot actuation | There is no `setVoltage`, no `setSetpoint`. The absence *is* the archetype. A `VisionIO` that exposed an actuation method would be a category error — vision being asked to drive something. ### The observation carries evidence, not an answer The inputs do not hand down a single trusted pose. They hand down the *evidence* so the subsystem can decide how much to trust it: a per-frame timestamp, the estimated pose or poses, which tags were used, and the ambiguity and average tag distance that downstream code turns into a standard-deviation weight. 6328 logs raw frames; 3061 logs computed `PoseObservation`s already carrying `averageAmbiguity` and `averageTagDistance` so the subsystem can reject or downweight bad frames. Here is the input struct from 6328 — note that what crosses the seam is observations, never commands: ```java public interface VisionIO { @AutoLog class AprilTagVisionIOInputs { public double[] timestamps = new double[] {}; // one row per frame, packed flat for NT/log transport: pose count, error, then the // candidate pose(s) as translation + quaternion, then the tag IDs used — decoded on the RIO public double[][] frames = new double[][] {}; // observations, not commands public long fps = 0; } // VisionIOInputs / ObjDetectVisionIOInputs are sibling structs of the same shape // (abridged — full file: RobotCode2025Public/.../vision/VisionIO.java) default void updateInputs(VisionIOInputs inputs, AprilTagVisionIOInputs aprilTagInputs, ObjDetectVisionIOInputs objDetectInputs) {} default void setRecording(boolean active) {} // config, not actuation } ``` The struct omits any `PhotonCamera` or `LimelightHelpers` type, any drivetrain reference, and any game logic. It is timestamps, poses, and tag evidence. ### The Photon implementation confines the vendor The vendor SDK lives in exactly one file. This is the 3061 RIO-side implementation: ```java import org.photonvision.PhotonCamera; // ◀ the ONLY place a vendor vision type appears import org.photonvision.PhotonPoseEstimator; public class VisionIOPhotonVision implements VisionIO { protected final PhotonCamera camera; protected final PhotonPoseEstimator photonEstimator = // strategy fixed at construction new PhotonPoseEstimator(fieldLayout, PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, robotToCamera); @Override public void updateInputs(/* ... */) { for (PhotonPipelineResult result : camera.getAllUnreadResults()) { Optional visionEstimate = photonEstimator.update(result); // ...compute averageAmbiguity, averageTagDistance per observation, // push PoseObservation(pose, timestamp, stdDevs) into the inputs... } } } ``` `org.photonvision` appears here and nowhere else. A `VisionIOLimelight` is a sibling file implementing the same contract with `LimelightHelpers` and MegaTag2 — 254 and many others run this variant. When a team swaps Photon for Limelight, they write the sibling and change which IO they construct. The subsystem, `RobotState`, and the whole robot stay the same. > **MegaTag2 is not a symmetric swap.** It must be seeded with the robot's gyro heading every cycle (`LimelightHelpers.SetRobotOrientation(...)`), and because its solve is rotation-constrained by that seed, teams typically fuse only x/y and inflate the θ std-dev. The IO contract holds; the sibling file carries this extra choreography. The discipline, stated as a rule: banned above the IO line are `org.photonvision.*` and `LimelightHelpers`. They live only in `VisionIOPhotonVision` / `VisionIOLimelight`. Allowed above the line are WPILib geometry types (`Pose2d`, `Transform3d`) and your own observation record. ### A sim variant for a sensor A sensor has no physics plant, so "sim" does not mean modeling dynamics. It means producing fake observations. A `VisionIOSim` either renders tags from a simulated field pose with PhotonLib's `PhotonCameraSim`, or it replays logged frames. Either way it implements `VisionIO` and pushes observations into the same input struct, so the subsystem above it cannot tell sim from hardware — the same sim-parity property the rest of Part II relies on. This is why a logged match is so useful for vision: a recorded match *is* a stream of real observations. Replayed through the same code under AdvantageKit, it reproduces every fusion decision the robot made, which makes vision logic testable off-robot with no camera and no field. ## The consumer — `RobotState.addVisionMeasurement` The subsystem's one downstream coupling is to the state seam. The Vision subsystem calls the vision-measurement method — `addVisionObservation` in 6328's hand-rolled form, shown here; `RobotState` blends the measurement into its pose estimator, weighted by the standard deviations the observation carried: ```java public void addVisionObservation(VisionObservation observation) { /* fuse into pose estimator */ } public record VisionObservation(Pose2d visionPose, double timestamp, Matrix stdDevs) {} ``` This is the seam from [chapter 4](../part-1/04-the-state-seam.md) and the world model of [chapter 20](20-the-world-model.md). Vision attaches here, and *only* here. The coupling is intended — feeding `RobotState` is vision's whole purpose — but it should be a method on a small interface rather than a reference to the concrete `RobotState`, so the `vision/` package lifts out as a library. The package may import WPILib and, in the IO implementation, the vendor SDK; it must not import `Drive` or any other subsystem. At D7 L2–L3, `RobotState` typically wraps WPILib's `SwerveDrivePoseEstimator`; at L4 some teams hand-roll the equivalent ([chapter 20](20-the-world-model.md)). Either way, the `stdDevs` matrix sets, per measurement, how much the estimator should trust this vision pose against the wheel odometry it already has. A measurement with small std-devs pulls the estimate toward the vision pose; one with large std-devs barely moves it. The trust gate, then, is mostly a matter of choosing those std-devs well — or refusing to fuse at all. > **Timestamp epoch — the most common silent failure.** The measurement timestamp must be on the same clock as the estimator. NetworkTables time, FPGA time, and CTRE's time-synced clock are different epochs; CTRE's swerve API provides `Utils.fpgaToCurrentTime(...)` for exactly this conversion. Fuse with the wrong epoch and every measurement rewinds to the wrong moment — the pose quietly degrades with no error anywhere. ## The trust gate — std-dev and ambiguity gating This is the rung that separates a team that merely calls `addVisionMeasurement` from one that does localization well. AprilTag pose estimates are not uniformly reliable. A tag seen edge-on, far away, or alone produces an ambiguous pose — the solver cannot tell which of two orientations is correct. Ambiguity is a *single-tag* concept: with one tag the PnP solver faces two mirror solutions, while a multi-tag solve has no pose ambiguity at all — which is why multi-tag observations earn trust. Fusing a bad frame blindly jumps the robot's belief and breaks aiming and auto. The gate weights or rejects each observation *before* it reaches the estimator. The discriminating markers, from the rubric, are `setVisionMeasurementStdDevs` (dynamic per-measurement std-dev tuning) and a `VisionIO` interface with a sim variant (vision decoupled from `Drive`). The inputs carry `averageAmbiguity` and `averageTagDistance` precisely so this gate has something to act on. A typical policy: reject observations above an ambiguity threshold outright; for the rest, scale the std-devs up with tag distance so far tags count for less; trust multi-tag solutions more than single-tag ones. A concrete anchor for that policy is a formula of the shape `stdDev = base * avgTagDistance² / tagCount` — trust falls with the square of distance and rises with the number of tags. 3061 and 254 both weight and reject by ambiguity and tag distance before fusing. ## The D7 rubric ladder Dimension D7 of the [rubric](../appendices/how-we-developed-this/02-the-rubric.md) measures what the robot believes about where it is and how that belief is maintained. The vision system maps onto its levels directly: | Level | Anchor | What it looks like in the vision system | |---|---|---| | 0 | None | No vision; odometry unused in decisions | | 1 | Targeting only | Limelight `tx`/`ty` servoing on a target; no pose estimation | | 2 | Pose estimation | AprilTag pose feeding `addVisionMeasurement` — the near-universal floor (50/55) | | 3 | Fused, filtered | std-dev tuning, multi-camera fusion, rejection logic; vision behind an IO interface with a sim variant | | 4 | World model as architecture | a dedicated `RobotState` owning pose + game-piece state with time-interpolated buffers; localization decoupled from control | The level is not set by whether `addVisionMeasurement` is called — it nearly always is. It is set by the *filtering* (the trust gate of the previous section) and the *architecture* (the IO seam and the central world model). Both must be read in the code to score, because `addVisionMeasurement`'s presence is the L2 floor and says nothing about L3. The corpus prevalence backs this: `addVisionMeasurement` in 50 teams, but a `*PoseEstimator` in 36 and a `*RobotState` class in only 26 — pose estimation is far more common than a centralized world model, so the L2-vs-L4 split is real. ## FRC localization is a known-map problem Why AprilTag fusion rather than SLAM? The field geometry ships as CAD months before the season, so the robot does not need to build a map of an unknown environment — it needs to find itself on a map it already has, which is exactly what fiducial pose fusion does. SLAM solves a harder problem the field does not pose; the fuller argument is in the [Lessons from Outside appendix](../appendices/lessons-from-outside/01-lessons-from-outside.md). ## The rungs past AprilTag fusion Two techniques layer on top of the AprilTag floor for teams that have reached it. **Neural game-piece detection.** Beyond pose, teams run an on-coprocessor object detector — a neural model on a Limelight or PhotonVision pipeline — that reports the bearing, and sometimes the range, to game pieces. A "drive-to-piece" auto or teleop-assist routine then turns to and intakes the best available target. The detector is mainstream: **31 teams** carry one (including 254, 971, 1678, 2056, 2637). The detector itself is the easy half; the differentiating part is the architecture on top — closed-loop drive-to-piece with rejection of bad detections and a graceful fallback when nothing is seen. Architecturally this is the same shape as AprilTag vision: a sensor that produces an observation (here a bearing instead of a pose) crossing the same kind of IO seam. **QuestNav.** A Meta Quest headset is a cheap, very good inside-out 6-DOF tracker — it has to be, to render VR without nausea. QuestNav mounts one on the robot and streams its pose over NetworkTables, where it is fused with AprilTag vision as a high-rate, low-drift odometry source. It is the newest item here — **1 team** in the corpus (7028's 2026 season code, using the `gg.questnav` library) — and emerging across FRC for 2026. The point for this chapter: the Quest does not need a new architecture. It is one more observation feeding the same `RobotState` pose estimator, behind the same kind of IO seam. ## What the vision system produces, and who consumes it The whole system, read by output, comes down to four things crossing into the rest of the robot — and each has a specific consumer: | Output | Carried as | Consumed by | |---|---|---| | Field-relative pose estimate | `Pose2d` + timestamp | `RobotState`'s pose estimator (the fusion) | | Standard deviations | `Matrix` | the fusion's per-measurement trust weighting | | Latency timestamp | `double` seconds | the time-interpolated buffer, so the measurement applies at the moment it was captured, not when it arrived | | Target bearing (neural detector) | angle, optional range | the aim — drive-to-piece auto and teleop-assist | The pose estimate and its std-devs and timestamp go to the fusion in `RobotState`. The fused pose then reaches the aim and the auto, but indirectly: they call `RobotState.getPose()`, never the camera. The neural detector's bearing is the one output that drives an aiming behavior directly. Nothing in this list is an actuation command — the vision system observes and reports, and the consumers decide what to do. ## Checklist - [ ] A `VisionIO` whose methods are `updateInputs(...)` plus camera config — no actuation method. - [ ] Observations carry timestamp, pose, and ambiguity/tag-distance, so they can be weighted. - [ ] The implementation (`VisionIOPhotonVision` / `VisionIOLimelight`) is the only file importing `org.photonvision` / `LimelightHelpers`. - [ ] Vision feeds `RobotState`'s vision-measurement method (`addVisionMeasurement`, or a hand-rolled `addVisionObservation`) and never references the drivetrain. - [ ] High-ambiguity or far-tag observations are rejected or downweighted before fusing (the D7 L3 gate). - [ ] A test publishes a known observation and asserts the fused pose responds, and that a bad frame is rejected. --- The vision system produces a belief and the trust to attach to it; the [world model](20-the-world-model.md) holds that belief over time, and the coordinator decides what to do with it. The other archetypes that feed the same seams are in [chapter 18](18-subsystem-archetypes.md); the drivetrain that reads the fused pose is in [chapter 19](19-the-drivetrain-subsystem.md). For how the actuating subsystems get their commands, see [the control path](15-control-path.md), [hardware abstraction](16-hardware-abstraction.md), and [motor interfaces](17-motor-interfaces.md). NEXT: [22. Coordination — state machines](22-coordination-state-machines.md), where the executive layer turns the world model into intent. Graphs and trees follow in [chapter 23](23-coordination-graphs-trees.md). # 22. Coordination I — state machines and the superstructure https://frc-code-scout.jointheleague.org/part-2/22-coordination-state-machines/ *Part I's chapter on [the coordination seam](../part-1/05-the-coordination-seam.md) named six paradigms and deferred their construction. This chapter builds the common ones: the superstructure, and the two finite-state-machine shapes the corpus uses to run it. The far end of the spectrum — transitions as data, searched as a graph or walked as a behavior tree — is the [next chapter](23-coordination-graphs-trees.md).* Build the contract for your robot. This chapter is the deep dive [chapter 5](../part-1/05-the-coordination-seam.md) pointed to. Part I argued why a coordination seam exists and where it sits relative to the [control path](15-control-path.md), the [subsystem archetypes](18-subsystem-archetypes.md), and the [world model](20-the-world-model.md). It will not re-argue that here. What follows is the engineering: the I/O boundary the seam presents, the three implementations the corpus actually ships, the maturity ladder they fall on, where kinematic interlocks live, and how you test the safe ordering without a robot. --- ## 1. The seam, stated as a boundary The superstructure turns one robot-wide goal into a coordinated set of subsystem setpoints, through a single transition function that is allowed to reorder or reject a transition for safety. A button asks for `SCORE_L4` (the game's level-4 scoring target — no relation to rubric levels, which this chapter writes D2-level-3 style); the superstructure decides the legal sequence — clear the frame, raise the elevator, then score — that realizes it. It is the one object that sees every mechanism at once, so the knowledge that "the arm must clear before the elevator rises" lives in exactly one place instead of scattered across subsystems. Like [RobotState](20-the-world-model.md), the superstructure is a "subsystem" with no hardware. Its I/O is goals in, subsystem setpoints out: ``` Operator/Auto → requestGoal(Goal) → [ one guarded transition function ] → subsystem.setState(...) ×N reads RobotState for safety/sequencing ``` The split is intent vs execution: a caller expresses *what it wants* (a goal) and walks away; the superstructure owns *how each mechanism gets there* (the legal, sequenced setpoints). Callers cannot drive a mechanism into an illegal configuration because only the transition function writes setpoints. There is no control loop here — each subsystem still closes its own loop behind its [IO line](16-hardware-abstraction.md) and its [motor interface](17-motor-interfaces.md). The superstructure only decides which setpoint each subsystem should have right now. ```d2 direction: right start: "" { shape: circle } STOW INTAKE SCORE_L4: "SCORE_L4 (guarded: arm clears frame before elevator raises)" start -> STOW STOW -> INTAKE: request INTAKE INTAKE -> STOW: has piece STOW -> SCORE_L4: request SCORE_L4 SCORE_L4 -> STOW: scored ``` ### 1.1 The contract | Method | Direction | Why | |---|---|---| | `requestGoal(Goal)` / `setStateCommand(State)` | in | a caller asks for one robot-wide state; returns a `Command` | | the transition function (`applyGoal` / `updateSubsystemStates` / `continueTransition`) | internal | the seam — fans the goal out to subsystem setpoints, with guards | | `getState()` / `atGoal()` | out | what the robot is doing now (read by auto, LEDs, RobotState) | What it omits is as load-bearing as what it has. No `TalonFX`, no `*IO` interface, no `XxxIOSim` / `XxxIOReal`. It holds the *subsystems* — their public `setState` / `setGoal` API — not their hardware. No motor math, no vendor SDK. Pure command and decision logic. A vendor type appearing here means a subsystem has leaked its hardware upward, and the corpus exemplars in this chapter are clean: 3128 and 3476 import zero vendor types. --- ## 2. How wide a paradigm this is This is the richest point of architectural divergence in FRC. The class-name counts come from the 55-repo season index: a class named `Superstructure` appears in 22 of 55 repos, a generic `*StateMachine` in 12 of 55. (The [next chapter](23-coordination-graphs-trees.md)'s index table puts `Superstructure` coordinators at 28 — that broader marker counts any class playing the coordinator role, whatever it is named; the 22 here is the literal class name.) The hand survey of 37 teams is what read past the names — it identified six coordination paradigms, with an orthogonal centralized-vs-distributed axis layered on top: | Paradigm | Exemplars | Best at | |---|---|---| | Command composition | 254, 1155, 3061, 190 | resource arbitration; composing reusable command objects | | Wanted/current FSM (distributed) | 2910, 33, 4099, 4504 | localizing each mechanism's intent-vs-execution split | | Centralized `RobotManager` FSM | 581, 3128 | reasoning about whole-robot states in one place | | State graph (search over states) | 6328, 254 | complex transition logic; pathfinding through legal states | | Behavior tree | 3015 | deeply nested, reactive decision logic borrowed from game AI | | Message passing (inter-process) | 971 | process isolation; language-agnostic contracts | The first three rows, plus the boundary of the fourth, are this chapter. The state graph and behavior tree are [chapter 23](23-coordination-graphs-trees.md). The survey's framing matters for how you read the rest: these are not competitors to choose among once. They are rungs and forks. A program grows from command composition, to a distributed or centralized FSM, to a state graph or behavior tree when transitions outgrow a switch statement. --- ## 3. Centralized FSM — one goal fans out to dumb subsystems The first shape the corpus uses is a single object holding the whole robot's state machine, driving subsystems that deliberately expose only state setters. The survey calls this the 581/3128 pattern: "a single `RobotManager` holds the entire robot's state machine and drives a set of deliberately 'dumb' subsystems that only expose setters. Mechanism behavior lives in one place; subsystems own no decision logic." It is a genuine architectural fork from the distributed FSM — the centralized version "makes whole-robot states trivial to reason about but concentrates all complexity in one file." The goal is an enum where each robot state bundles a target state for every subsystem. 3128's `RobotManager` extends an FSM base and fans one `RobotStates` value out. (Despite the name, 3128's `RobotStates` is a goal enum — no relation to the `RobotState` world-model class of [chapter 20](20-the-world-model.md).) ```java // 3128 Aluminum Narwhals — subsystems/Robot/RobotManager.java public class RobotManager extends FSMSubsystemBase { private static Elevator elevator; private static Manipulator manipulator; private static Intake intake; private static Climber climber; private static Swerve swerve; // THE SEAM: one robot goal -> a sequenced, guarded fan-out to each subsystem public Command updateSubsystemStates(RobotStates nextState) { return sequence( elevator.setStateCommand(nextState.getElevatorState()) .unless(() -> nextState.getElevatorState() == ElevatorStates.UNDEFINED), manipulator.setStateCommand(nextState.getManipulatorState()).unless(/* ... */), intake.setStateCommand(nextState.getIntakeState()).unless(/* ... */), climber.setStateCommand(nextState.getClimberState()).unless(/* ... */), waitUntil(() -> climber.winch.atSetpoint()) // ◀ a guarded transition .unless(() -> nextState.getClimberState() == ClimberStates.UNDEFINED)); } } ``` Every transition routes through `updateSubsystemStates`. Three details carry the technique: - **One enum per subsystem, bundled by the robot state.** `nextState.getElevatorState()`, `getManipulatorState()`, and so on each return that subsystem's target. The robot-wide goal is a tuple of per-subsystem goals, and the fan-out is mechanical: read the target for each subsystem, command it. - **`UNDEFINED` means "leave this one alone."** The `.unless(...)` guards skip any subsystem whose bundled target is `UNDEFINED`, so a robot state can move some mechanisms and leave the rest untouched. This is how one transition function serves many goals without a combinatorial mess of full-robot poses. - **The `waitUntil(...)` is the interlock seam.** Sequencing the climber winch to reach its setpoint before the transition completes is the guarded transition: the safe order is expressed here, once, and the subsystems stay dumb. The imports are exactly what the contract predicts — WPILib commands, the team's own subsystems and `common` FSM library, and not one vendor type. The decision logic and the hardware never meet. One caveat the corpus build-spec flags: 3128 reaches its subsystems through static fields rather than constructor injection. That works on the robot but makes the seam harder to test in isolation, because a test cannot hand it sim-backed subsystems. Section 6 returns to why injection matters for the interlock test. --- ## 4. Superstructure-as-`SubsystemBase` — the transition runs over time The second shape makes the superstructure a real `SubsystemBase` that holds the mechanisms plus a state machine, accepts a target state, and advances the transition each cycle in `periodic()`. This is the 3476 style, and the survey places 3476 with 254 and 6328 at the level that "separates intent from execution and handles kinematic safety," distinct from the centralized-FSM fork: ```java // 3476 Code Orange — superstructure/Superstructure.java public class Superstructure extends SubsystemBase { private Elevator elevator; private EndEffector endEffector; private SuperstructureStateMachine stateMachine; @Override public void periodic() { stateMachine.continueTransition(); // advance the guarded transition RobotState.setSuperstructureState(getCurrentState()); // publish intent to the world model Logger.recordOutput("Superstructure/TargetState", stateMachine.getTargetState()); } public Command setStateCommand(SuperstructureState state, String name) { return new InstantCommand(() -> stateMachine.setTargetState(state)).withName(name); } } ``` The difference from the centralized FSM is the time axis. A goal request here is just "set the target state" — an `InstantCommand` that flips one field and returns. The work happens in `continueTransition()`, called every cycle, walking the legal path toward the target one step at a time. Where 3128's `sequence(...)` builds the whole ordered transition as a command up front, 3476's `continueTransition()` is a tick function: each `periodic()` it looks at where the robot is, where it should be, and takes the next safe step. Two things this buys, both visible in the four lines of `periodic()`: - **Intent is published to the world model.** `RobotState.setSuperstructureState(getCurrentState())` writes what the superstructure is doing into [RobotState](20-the-world-model.md), so auto routines, LEDs, and dashboards read the robot's coordination state from one place rather than poking the superstructure directly. The seam announces its execution; the rest of the robot subscribes. - **The target is logged separately from the current.** `Logger.recordOutput("Superstructure/TargetState", ...)` records intent next to execution, which is exactly what you want when a transition stalls and you are trying to see whether the goal was wrong or the path to it was blocked. The survey also notes 3476's refinement: they "expose superstructure states as autonomous actions (`SetSuperstructureState`, `WaitForSuperstructureState`)," which marries 254's action-based autonomous to the request-based superstructure. The auto routine asks for a state and waits for it; it never sequences the mechanisms itself. Because 3476 takes its subsystems by constructor, a test can build the whole superstructure on sim-backed mechanisms — the property 3128's static fields give up. --- ## 5. The wanted/current two-enum pattern Underneath both shapes is a single idea, stated most plainly by 2910: hold two states per mechanism, the one requested and the one actually in effect, with a transition function as the only thing allowed to move between them. The survey calls 2910 "the reference implementation of the wanted/current state-machine pattern," and notes the reversal worth marking — 2910 is the team most cited for running finite-state machines instead of the command scheduler, but their 2025 code is a hybrid. Subsystems do extend `SubsystemBase` and use an IO layer (`ShoulderIO` / `ShoulderIOTalonFX`); behavior is driven by explicit state machines rather than scheduled commands. "FSM vs command-based" is a false binary; the strongest version uses command-based for resource arbitration and an FSM for decision logic. The pattern, stated precisely, is two enums and one function: ```java // 2910 — every subsystem and the superstructure follow this shape enum WantedState { IDLE, HOME, INTAKE, SCORE_L4, ... } // requested enum SystemState { IDLING, HOMING_WRIST, ... } // actual private SystemState handleStateTransitions() { switch (wantedState) { case HOME: ... return SystemState.HOMING_SHOULDER; ... } } // each periodic(): currentState = handleStateTransitions(); applyStates(); ``` A caller sets `wantedState` and walks away. It never knows what sequence of physical steps the subsystem must run to honor the request, and it cannot put the mechanism into an illegal configuration because only `handleStateTransitions()` writes `SystemState`. The superstructure does the same at the next level up — its wanted state is a robot-wide goal, its current state is what it has actually achieved, and the same transition function mediates between them. A vocabulary note: *wanted* (2910), *target* (3476), and *goal* (3128, and the rubric's word) all name the same half of the pair — the requested state, as distinct from the achieved one. This is the in-process equivalent of 971's `Goal` / `Status` message split, achieved with two enums inside one process. The distributed form (2910, 33, 4099, 4504) localizes each mechanism's intent-vs-execution split — one wanted/current machine per subsystem, plus one on top. The centralized form (581, 3128) collapses the per-subsystem machines into a single `RobotManager`. Same intent-vs-execution principle; opposite choices about where the machines live. Two notes on where this pattern shows up in other languages, both from the survey: 4504's Python MagicBot provides the wanted/current machinery as a framework feature — "a dumb `launcher.py` (just hardware and setters) and a `launcherController.py` subclassing MagicBot's `StateMachine` with `@state`-decorated methods and `next_state()` transitions" — the same pattern with the switch statements provided by the framework. And 4099 reaches it in Kotlin as a "request-based superstructure," reinforcing that the design is language-independent. --- ## 6. The D2 ladder These implementations are not interchangeable; they are rungs on the rubric's coordination dimension. Reading the corpus build-spec and survey together, the coordination axis (rubric D2) has four levels: | Level | Shape | Team | |---|---|---| | 1 | command composition — sequential/parallel groups; no coordinator | most teams | | 2 | wanted/current FSM, distributed (one per subsystem + a top one) | 2910, 4099 | | 2 | centralized `RobotManager` — one FSM, dumb subsystems | 581, 3128 | | 3 | `Superstructure` coordinator that separates intent from execution + handles kinematic safety | 254, 3476, 6328 | | 4 | state graph — transitions are data; pathfind through legal states (JGraphT / A*) | 6328, 254 | The progression is a sequence of refactors a team performs when the previous rung breaks down, each motivated by a problem just felt: - **Level 1 → 2.** Command composition works until two callers want conflicting things or a caller can sequence mechanisms into an illegal pose. The fix is an FSM: callers request intent, a transition function owns execution. The wanted/current two-enum machine of §5 is the right *first* state machine — it teaches the core idea without graph-search machinery. - **Level 2 → 3.** Distributed or centralized FSMs handle each mechanism, but kinematic safety — two mechanisms that share space and can collide — needs an object that sees both. A `Superstructure` coordinator separates "which pose we want" from "the safe path to get there," and owns the interlocks. - **Level 3 → 4.** When transitions get complex enough that the legal sequence between two states is itself a search problem, the transition logic becomes data: a graph of states with commands on the edges, walked by a path search. That is the optimization for when a switch statement stops scaling — and the subject of the [next chapter](23-coordination-graphs-trees.md). The two level-2 rows are the centralized/distributed fork, not a quality difference. Both are intent-vs-execution machines; they differ in where the machines live. --- ## 7. Where the interlocks live, and how you test them The defining job of a level-3 superstructure is kinematic safety: ensuring two mechanisms that can collide never do. The architectural rule is that this knowledge lives in the *one* transition function, as guards and sequencing — not scattered across subsystems. 3128's `waitUntil(() -> climber.winch.atSetpoint())` is one such guard. The general form is "the arm must clear the frame before the elevator rises," expressed as ordering inside the seam. (Some teams pull the geometry out further still — 1678's dedicated `MotionPlanner` owns collision-free arm/elevator motion as its own layer; [chapter 23](23-coordination-graphs-trees.md) treats it in full. Here the interlock lives directly in the transition function.) Interlocks are exactly what you want to test, and the superstructure is the one place they live, so it is the one place to test them. Because the subsystems below it run on their `*IOSim` implementations, you construct the whole thing in sim, request a dangerous goal, and assert the *order*: ```java // construct subsystems on Sim, build the superstructure, request a dangerous goal: var sup = new Superstructure(Elevator.create(), Arm.create() /* both Sim in a test */); run(sup.requestGoal(SCORE_L4)); // step the sim to completion, recording the first tick each threshold was crossed // (firstTick is an abridged test helper — poll the condition while fast-forwarding) double tArmClear = firstTick(() -> arm.getAngle() > ARM_CLEAR_ANGLE); double tElevRise = firstTick(() -> elevator.getHeight() > DANGER_HEIGHT); // assert the guard held: the arm cleared the frame BEFORE the elevator entered the danger zone assertTrue(tArmClear < tElevRise); // the interlock, verified without a robot ``` This is the test that catches the failure mode that breaks robots — two mechanisms colliding — and it costs nothing once the subsystems have sim implementations. It also explains why §3 flagged 3128's static fields and §4 noted 3476's constructor injection: the test above only works if you can hand the superstructure sim-backed subsystems. A superstructure that reaches its mechanisms through `getInstance()` cannot be constructed against sim in a test. Take subsystems by constructor (3476, and 5190 below) rather than `getInstance()` (3128), and the interlock test is available; skip injection, and it is not. The survey backs the payoff: SciBorgs (1155) run 14 JUnit suites that build sim-backed subsystems and run real commands to completion, and Ranger Robotics (3015) ship 37 test files. The interlock test is one instance of the IO layer's deferred dividend — verification on CI before the code reaches a robot. --- ## 8. The anti-pattern: a level-1 class wearing a level-3 name A class named `Superstructure` is not a coordinator if it just holds references and exposes manual jogs. The build-spec calls this "level-1 wearing a level-3 name," and 5190 is the example of what not to ship: ```java // 5190 Green Hope Falcons — Superstructure.java (what NOT to ship) public class Superstructure { public final Pivot pivot_; public final EndEffector end_effector_; public Command jogPivot(double percent) { /* StartEndCommand setPercent */ } // no goal, no transition } ``` There is no goal enum, no transition function, no interlock — it is a bag of subsystems with jog buttons. The name promises a coordination seam; the body delivers a holder. This matters for scoring: D2 is scored on what the class *does*, not what it is *named*. Before crediting a superstructure with level-3 coordination, confirm three things by opening the file: - **An actual goal-request API** — a goal/state enum and a `requestGoal` / `setStateCommand` method, not just per-subsystem jog buttons. - **A single transition function** that all transitions route through, fanning the goal out to subsystem setpoints. - **Kinematic interlocks living here**, as guards or sequencing, not pushed down into subsystems. 5190 has the public-field holder and the jog, and none of the three. It is a level-1 command-composition setup with a level-3 name — exactly the case the rubric tells you to catch by reading the code rather than grepping for the class name. --- ## 9. Vendor and IO discipline at the seam The coordination seam is, like RobotState, naturally vendor-free, and a leak here is a loud signal that a subsystem's abstraction is broken. The rule: > The superstructure imports subsystems, not their hardware. No `com.ctre` / `com.revrobotics` / `org.photonvision`, no `ElevatorIOTalonFX`, no `ElevatorIO`. If a vendor type or an IO impl appears here, a subsystem has leaked its hardware upward — the fix is to expose a `setGoal(...)` / `setState(...)` on the subsystem and call that. The corpus exemplars above are clean: 3128 and 3476 import zero vendor types. The superstructure depends only on WPILib commands, the subsystems' public API, and [RobotState](20-the-world-model.md). Its one coupling is to the mechanisms it coordinates — which is correct; that is its whole job. The discipline is identical to the one drawn at the [IO line](16-hardware-abstraction.md) and the [motor interface](17-motor-interfaces.md): a vendor type belongs below the IO line, and the coordination seam sits well above it. The seam imports the *public state API* of each subsystem — `setState`, `setGoal`, `getState` — and never an IO implementation. ### Checklist — is your coordination seam intact? - [ ] A goal/state enum and a `requestGoal` / `setStateCommand` API — not just per-subsystem jog buttons. - [ ] **One** transition function fans the goal out to subsystem setpoints; all transitions route through it. - [ ] Kinematic interlocks (mechanism-collision safety) live **here**, as guards/sequencing — not in subsystems. - [ ] It holds subsystems (their `setState` API), **no `*IO` impl and no vendor type**. - [ ] Subsystems are injected by constructor so a test can pass sim-backed ones. - [ ] A test requests a dangerous goal and asserts the safe ordering held (the interlock). - [ ] Intent is logged and written to [RobotState](20-the-world-model.md) so auto, LEDs, and dashboards can read it. --- ## 10. Where this sits The three implementations here — centralized FSM (§3), superstructure-as-`SubsystemBase` (§4), and the wanted/current two-enum machine (§5) — are the common rungs of the D2 ladder, levels 2 and 3. They share one principle: one robot-wide goal in, guarded subsystem setpoints out, through a single transition function, with intent separated from execution. They differ in where the machines live (centralized vs distributed) and whether the transition is built up front as a command or advanced each cycle as a tick function. The far end of the ladder — level 4, where transitions become data and the legal path between two states is found by search — is a different enough technique to warrant its own chapter. The state graph (6328's JGraphT, 254's A\*) and the behavior tree (3015's leaf/decorator/blackboard runtime) are coordination for when a switch statement stops scaling. Continue to the next chapter: [23. Coordination II — state graphs and behavior trees](23-coordination-graphs-trees.md). # 23. Coordination II — state graphs and behavior trees https://frc-code-scout.jointheleague.org/part-2/23-coordination-graphs-trees/ *The wanted/current FSM and the centralized `RobotManager` of [the previous chapter](22-coordination-state-machines.md) are where most elite teams stop. This chapter walks the two rungs past them: modeling the superstructure as a graph of legal states and searching for a safe path, and ticking a behavior tree as a reactive whole-robot brain. [Chapter 5](../part-1/05-the-coordination-seam.md) drew the coordination seam and [chapter 8](../part-1/08-alternatives.md) pointed past it to both paradigms as sound-but-uncommon. Part I argued *why* a team would climb here; this chapter shows the machinery and, for each rung, says plainly when it earns its complexity and when it is over-engineering.* The two paradigms live at different layers and compose rather than compete. The state graph is a mechanism-layer answer to "how do I move the superstructure there *safely*?" — established but uncommon in the corpus, with configuration-space A\* as its far edge (effectively 254 alone). The behavior tree is a strategy-layer answer to a higher question — "what should the robot *pursue* right now?" — and is rarer still (one team, 3015). Beneath both sits the wanted/current FSM of [chapter 22](22-coordination-state-machines.md), the default coordination rung among elite teams. A clean stack would run top to bottom — a behavior tree decides intent ("score high"), the superstructure, graph or FSM, executes that intent safely — but no corpus team actually ships that composed stack. It is plausible, not observed. --- ## Part 1 — Coordination as graph search ### The N² problem the graph dissolves Hand-coded coordination is an N² problem. For every `(from-state → to-state)` pair you write the ordered, interlock-safe sequence by hand, and get some of them subtly wrong. A robot with a stow state, a coral-intake state, and four scoring levels has on the order of `6 × 5 = 30` ordered transitions, each one a small sequence of "retract the wrist before the elevator moves, then raise, then extend." Every one is a place to forget an interlock. Add a state and you add transitions to and from every existing state. Model it as a graph instead: - **nodes** are states, - **edges** are legal single moves, and - **an edge exists only if that move is collision-free.** You declare the safety rules once, locally, at edge-construction time, then let a shortest-path search compose any `from → to` request into a globally safe sequence. The interlocks stop being scattered `if` checks spread across thirty transition bodies and become *edge existence*. If a move would collide, the edge is not in the graph, so no path can route through it. That representation is the payoff; the search algorithm is a detail set by graph size. ```d2 direction: right STOW -> CORAL_INTAKE STOW -> L1 CORAL_INTAKE -> STOW CORAL_INTAKE -> L2 L1 -> L2 L2 -> L3 L3 -> L4 L4 -> L3 L2 -> STOW L4 -> STOW: "blocked: wrist clips elevator" { style.stroke-dash: 3 } ``` The dashed edge is the point: `L4 → STOW` directly would clip the wrist on the elevator, so that edge is simply absent. A request to go from `L4` to `STOW` finds the path `L4 → L3 → L2 → STOW` automatically. Nobody wrote that sequence. The search found it because those are the only edges that exist. ### The three rungs — climb only as far as collision complexity forces The source survey lays out three rungs and is explicit that you should climb only as far as the mechanism's self-collision coupling demands. **Rung 1 — flat enum FSM.** `Stow / Intake / Score` plus a few guards. Correct for a three-state robot with no self-collision coupling. This is the default and the subject of [chapter 22](22-coordination-state-machines.md), not this one. **Rung 2 — named-state graph + precomputed shortest path.** *Established in elite FRC, uncommon.* Roughly 10–30 hand-named states; BFS or Dijkstra computes all-pairs paths **once at startup**, giving O(1) deterministic lookup at runtime. At this scale A\* buys nothing — its heuristic only saves node expansions, and there are few nodes to expand. This is the tractable, provably-safe sweet spot. Start here. **Rung 3 — discretized configuration space + online A\*.** *Standard in general robotics, rare in FRC.* Grid the joint space (elevator × arm × wrist), make edges collision-free micro-moves, and run A\* with an admissible heuristic to plan a smooth, safe path **at runtime**. This buys two things the named-state graph cannot: **arbitrary** target configs rather than just named states, and **dynamic** collision geometry — a held game piece changes which edges exist, so you replan. The costs are bounded runtime inside the 20 ms loop, heuristic design, and a defined no-path fallback. ### The framing that makes A\* click The A\* leap sounds exotic until you notice teams already run it — on the drivetrain. PathPlanner's `pathfindToPose` is `LocalADStar` (Anytime Dynamic A\*), routing the chassis around field obstacles in real time. The corpus survey counts **35 teams** running an A\*-family planner on the drivetrain. The technique is proven and familiar. Rung 3 is the same algorithm in a different space. Instead of searching `(x, y, θ)` on the field where obstacles are walls and other robots, you search `(elevatorHeight, armAngle, wristAngle)` in configuration space where "obstacles" are self-collision regions. One layer up the robot, same idea. ``` drivetrain: A* over (x, y, θ) obstacles = field walls, robots superstructure: A* over (elev, arm, wrist) obstacles = self-collision volumes ``` ### Corpus reality — established but uncommon The survey's index counts, every one confirmed by reading source, place this paradigm precisely: | Marker | Teams | |---|---| | `Superstructure` coordinator (any kind) | 28 | | State-machine / `RobotManager` FSM | 17 | | Named-state enums | 34 | | Explicit state-graph / transition types | ~5 (190, 254, 2910, 3476, 5026) | | A\* over the superstructure | effectively 254 alone | | Dijkstra over the superstructure | 0 | | A\*-family on the drivetrain (PathPlanner ADStar) | 35 | One absence needs explaining: 6328, this chapter's worked example, is missing from the transition-type row. Its JGraphT-based graph is caught by a separate index marker — the `jgrapht` import, present in 3 teams — so the sweep for hand-rolled state-graph and transition types misses it by construction, not because the graph is unconfirmed. Two readings stand out. Dijkstra is at zero because at small graph sizes people hardcode or BFS — nobody reaches for a weighted shortest-path algorithm on a 20-node graph. And genuine A\*-over-the-superstructure is a corpus of one. ### 6328 — the superstructure as a literal directed graph Team 6328 (Mechanical Advantage) model the superstructure as a directed graph using the JGraphT library. States like `STOW`, `CORAL_INTAKE`, and `L4_CORAL` are graph vertices; the commands that move between them are edges. To reach a target the code runs a graph search and executes the edge commands along the resulting path. The shape, abridged from the survey's description: ```java // States are vertices; transition commands are edges. Graph graph = new DefaultDirectedGraph<>(EdgeCommand.class); graph.addVertex(SuperState.STOW); graph.addVertex(SuperState.CORAL_INTAKE); graph.addVertex(SuperState.L4_CORAL); // ... ~10-30 named states // An edge exists only for a legal, collision-free single move. // The interlock IS the edge: an illegal move is simply not added. graph.addEdge(SuperState.STOW, SuperState.CORAL_INTAKE, intakeEdge); graph.addEdge(SuperState.CORAL_INTAKE, SuperState.L4_CORAL, raiseEdge); // To honor a request: search, then run the edge commands in order. List path = DijkstraShortestPath.findPathBetween(graph, current, goal).getEdgeList(); ``` One abridgment to flag: the snippet closes with JGraphT's `DijkstraShortestPath` because it is the one-line way to show the search, but 6328's shipped code walks the graph with its own BFS — which is why the corpus table above scores "Dijkstra over the superstructure" at zero. The line worth dwelling on is the comment: *the interlock is the edge.* The survey states it directly — "the transition logic is data (a graph), not a tangle of if-statements." You never write `if (current == L4 && goal == STOW) { ... }`. You add the edges that are safe, omit the ones that are not, and the search composes the rest. ### 254 — A\* over the state graph Team 254 take the same "superstructure = shortest path through a state graph" idea and name it outright. Their code carries an `AStarSolver`, an `AStarMap`, a `cachedAStar`, and a `SuperstructureStateMachine`, alongside a `CoralStateTracker` for game-piece state. The survey calls two of the strongest teams in FRC independently arriving at this representation the clearest convergence in the whole study. ```java // 254's coordination names the algorithm and caches the result. AStarSolver solver = new AStarSolver(stateGraph, heuristic); // cachedAStar: bound-and-cache online search so the 20 ms loop stays predictable. List path = cachedAStar.solve(current, goal); ``` The `cachedAStar` name is the design hinge made literal. Online A\* costs runtime; caching solved paths keeps the per-cycle cost bounded. This is the rung-3 discipline: precompute for small graphs, bound-and-cache online A\* for large ones. A neighbor pattern from Team 1678 (Citrus Circuits) is worth naming here even though it is not a graph. Where most teams fold collision avoidance into the state graph, 1678 pull it into a separate `MotionPlanner` that owns the geometry — including a command named `UnsafePivotAndElevatorSynchronousToPositionMotionMagic` that coordinates two axes so they arrive together without colliding. Separating "where we want to be" (the superstructure's state) from "what path through space is safe to get there" (the planner) is the same decomposition the graph makes, reached by a different route. ### Why the graph is worth it Three properties fall out of materializing the structure: - **It can be exhaustively tested where hand-coded transitions cannot.** With the graph as data, you can assert that every `(from, to)` path is collision-free and terminates, that the graph is connected (or that unreachable states are intentional), and that no edge violates an interlock. Hand-coded transition bodies offer no such handle — you can only test the cases you remember to write. This is the same materialize-the-structure-then-test-it payoff that simulation gets from a physical plant. - **It kills the N² hand-coding** and moves every interlock to one local place: edge construction. - **Replanning and interruptibility are clean.** You are always at a node or partway along an edge, so a new goal means "finish the current edge or safe-stop, then replan from the next node." ### Design hinges, decided when building out - **Edge cost = time-to-execute.** Then the search finds the *fastest* safe sequence, not merely the fewest steps. - **An A\* heuristic must be admissible** (for example, configuration-space distance divided by max joint speed) or you lose the optimality guarantee. - **Precompute for small graphs; bound-and-cache online A\* for large ones.** 254 literally ships `cachedAStar`. - **Define the no-path fallback** — hold, or route to a known-safe state. A blocked or disconnected graph is a real runtime state, not an impossible one. A held game piece can delete the edges that would otherwise get you home. ### When not to build a graph A few states with no real self-collision coupling means a flat enum FSM is correct, and the graph is over-engineering. The graph pays off only with many states, *real* interlocks, a wish for provable safety, or a need for arbitrary non-named target configs. The open question the survey leaves for any full build-out is which rung to center: the named-state graph (paved, deterministic, provably safe, A\* optional) or C-space A\* (arbitrary configs, dynamic obstacles, borrowed from general robotics). They share the "coordination = graph search" spine but diverge hard on cost and capability. --- ## Part 2 — Behavior trees The state graph answers a mechanism-layer question: how do I move there safely? A behavior tree answers a different, higher question: what should the robot be doing at all? It sits at the strategy layer. ### How a behavior tree works A behavior tree (BT) structures "what should the robot do right now?" as a tree that is re-evaluated — *ticked* — every control cycle. In WPILib terms, you tick the tree in `periodic()`, once per 20 ms loop. Each cycle the tree is ticked from the root down, and every node returns one of three statuses: - **SUCCESS** — done, it worked. - **FAILURE** — done, it didn't work. - **RUNNING** — still going, tick me again next cycle. `RUNNING` is the invention that makes the model work for robots. It lets a single action span many ticks: "drive to pose" returns `RUNNING` for as long as it takes, then `SUCCESS` once arrived, without blocking the loop. ### The node taxonomy **Leaves** do the work and come in two kinds. An **Action** runs intake or drives to a pose and returns `RUNNING` while busy. A **Condition** asks a question — have a piece? at setpoint? — and returns `SUCCESS` or `FAILURE` instantly. **Composites** are the control flow: - **Sequence** (`→`): tick children left to right; **fail fast**, succeed only if all succeed. Logical AND, "do in order." - **Selector / Fallback** (`?`): tick children left to right; **succeed fast**, fail only if all fail. Logical OR, "try in priority order." - **Parallel**: tick all children; resolve by a policy such as "succeed if M of N succeed." **Decorators** wrap a single child to modify its result: `Inverter`, `Repeat`, `Retry`, `Timeout`, `Cooldown`, condition guards. **The blackboard** is shared working memory. BTs are stateless control flow, so any shared data — target pose, have-piece — lives in a separate key-value store that nodes read and write. This is a real coupling surface, not a detail to wave away. ### The headline: reactive priority for free ``` Selector (priority — re-checked top to bottom EVERY tick) ├─ Sequence: [being rammed?] → [evade] ├─ Sequence: [have piece?] → [drive to goal] → [score] ├─ Sequence: [piece visible?] → [drive to piece] → [intake] └─ [drive to staging / idle] ``` Because the whole tree re-ticks from the root each cycle, the top selector re-evaluates priorities constantly. The instant `being rammed?` goes true, control jumps to *evade*; when it clears, control falls back to whatever the next-highest condition allows — with no explicit transitions wired. In an FSM you would hand-add an "→ evade" edge from every state, which is the N² transition problem from Part 1 in a new costume. In a BT, priority and preemption fall out of the tree's structure. Add a higher-priority branch at the top and every lower branch is preempted by it automatically. ### 3015 Ranger Robotics — the one full behavior-tree runtime Where every other surveyed team coordinates mechanisms with commands, a state machine, or a state graph, Team 3015 (Ranger Robotics) built a behavior-tree runtime in Kotlin under `lib/behaviorTree/` and run their robot on it. The source carries the textbook taxonomy: - **Leaf nodes:** `CommandRunner` (run a WPILib command as a tree leaf), `Wait`, `SetVariable`, `ClearVariable`. - **Decorator nodes:** `ConditionDecorator`, `InfiniteLoop`, `Loop`, `LoopUntilSuccess`, `ForceFailure`, `IsVarSet`. - **A blackboard:** shared named variables the nodes read and write. Each node returns `Success` / `Failure` / `Running` every tick, exactly as a behavior tree should. The library is unit-tested, and 3015 ship a separate visual `behavior_tree_editor` application to author the trees. The base shape, abridged: ```kotlin sealed interface NodeStatus object Success : NodeStatus object Failure : NodeStatus object Running : NodeStatus abstract class BehaviorNode { abstract fun tick(blackboard: Blackboard): NodeStatus } // A leaf that runs a WPILib command as a tree action. class CommandRunner(private val command: Command) : BehaviorNode() { private var started = false override fun tick(blackboard: Blackboard): NodeStatus = when { !command.isScheduled && !started -> { command.schedule(); started = true; Running } command.isFinished -> Success else -> Running } // abridged: 3015's real implementation also resets `started` on completion // and can return Failure (an interrupted command is not a success) } // A decorator that only ticks its child while a blackboard flag is set. class ConditionDecorator( private val key: String, private val child: BehaviorNode ) : BehaviorNode() { override fun tick(blackboard: Blackboard): NodeStatus = if (blackboard.isSet(key)) child.tick(blackboard) else Failure } ``` That `CommandRunner` leaf is the bridge: it lets the BT command WPILib's scheduler — the BT decides intent and the existing command-based machinery executes it. The survey places 3015 as the worked example for the rung where transitions outgrow a switch statement, and notes the library transfers directly to robotics and game AI outside FRC. ### You are already half-using one WPILib command-based is a behavior-tree cousin in disguise. The mapping is close enough to be uncomfortable: | Behavior tree | WPILib command-based | |---|---| | Sequence | `SequentialCommandGroup` | | Parallel | `ParallelCommandGroup` / `Race` / `Deadline` | | Selector | `ConditionalCommand` / `SelectCommand` | | Decorators | `.withTimeout()` `.until()` `.unless()` `.repeatedly()` | | RUNNING / SUCCESS | `isFinished()` + interruption | The corpus counts confirm the building blocks are everywhere and the assembled tree is not: | Marker | Teams | |---|---| | Explicit BT (BT-node / blackboard / tick tokens) | ~1 (3015) | | `SequentialCommandGroup` | 49 | | Parallel command groups | 37 | | `ConditionalCommand` / `SelectCommand` | 23 | The delta that makes a *real* BT interesting is not the node types — teams have those. It is how the tree is used. Teams use command groups for **fixed autonomous routines**: a `SequentialCommandGroup` is a pre-authored play that runs once. A behavior tree is used as a **whole-robot reactive brain** that re-decides every tick — "grab the closest piece unless defended, else reposition" — opportunistic play, not a scripted routine. The persistent reactive root tick plus a blackboard is the unexplored 20%. ### The costs, stated plainly A behavior tree is not free, and the survey is direct about the tradeoffs: - **"Where am I?" is fuzzier.** There is no single nameable current state. It is implicit in which leaves are `RUNNING` plus the blackboard contents. This is harder to debug and log than an FSM, where the current state is one enum value you can put on the dashboard. (3015 partly answer this with their visual editor.) - **State lives in the blackboard.** Because BTs are stateless control flow, all shared data sits in a separate key-value store that many nodes touch. That store is a real coupling surface, and it is the place bugs hide. - **The memory-sequence gotcha.** A naive Sequence re-ticks from its first child each cycle. You usually want a *memory* variant so completed steps do not re-run on the next tick. - **It does not plan.** A BT is a hand-authored reactive policy, not a planner. It picks *what to pursue*; it does not *compute* a safe path the way Part 1's A\* and state graph do. Its correctness is emergent from the tree's structure, not analyzable the way a graph's all-pairs paths are. This is exactly why the clean stack puts a BT on top of a graph, not in place of it. ### When a BT beats an FSM, and when it is over-engineering A BT earns its keep when the decision logic is deeply nested and reactive — many priorities that interrupt each other, where an FSM would need an "interrupt" edge from every state to every higher-priority behavior. It scales linearly: add a node, not edges to and from everything. It is over-engineering when the robot has a handful of states with clear linear transitions — there the FSM's single nameable current state is an asset, not a limitation, and the fuzziness of "which leaves are running" is pure cost. With explicit BTs at roughly one team in the corpus and command groups universal, the honest read is that most teams should keep using command groups for routines and an FSM for state, and reach for a reactive top-level tree only when opportunistic, preempting play is the actual problem. --- ## Where this leaves the coordination ladder Across [chapter 22](22-coordination-state-machines.md) and this chapter the full ladder is now in view: ``` command composition → wanted/current FSM → centralized RobotManager FSM → state graph (BFS/Dijkstra) → C-space A* (mechanism layer) → behavior tree as reactive brain (strategy layer) ``` Each rung is a refactor motivated by a problem the rung below could not absorb: hand-coded transitions get N²-tangled, so you make them data and search them; the decision logic itself gets too tangled for a switch, so you tick a tree. The mechanisms change every game; the ladder does not. This is the last chapter of Part II. Across these chapters the architecture appeared as a stack of named seams — the [control path](15-control-path.md), the [hardware abstraction line](16-hardware-abstraction.md), [motor interfaces](17-motor-interfaces.md), [subsystem archetypes](18-subsystem-archetypes.md), the [drivetrain](19-the-drivetrain-subsystem.md), the [world model](20-the-world-model.md), [vision](21-vision-systems.md), and coordination ([state machines](22-coordination-state-machines.md), then graphs and trees here). Each was treated as its own kind of thing with its own rules. [Part III — The League Architecture](../part-3/) argues they are not separate kinds of thing. It presents the unifying model in which every component — a motor, a subsystem, the superstructure, the whole robot — is one recursive shape repeated at different scales. The IO seam, the state seam, and the coordination seam you have been reading as nine distinct chapters turn out to be the same three joints, applied again and again. Read on there. # Part III — The League Architecture https://frc-code-scout.jointheleague.org/part-3/ # Part III — The League Architecture *The Elite Architecture, evolved. Parts I and II's components turn out to be instances of one shape; naming that shape is the whole proposal.* Part I reconstructed the architecture top teams converged on; Part II opened the hood on each piece. Part III is the only **prescriptive** part of the book. It keeps every Elite commitment — the IO seam, intent separated from execution, vendor confinement, the deferred-dividend discipline — and changes exactly one thing: it observes that the three differently shaped seams plus the pile of subsystems are all the **same recursive component**, and proposes building to that one contract deliberately. What every component shares is its **faceplate**: four serializable data objects plus one pure step. Naming that shape buys telemetry, replay, tests, lifecycle, and language portability **at every scale**, not just at the motor. The part states the shape, recovers the motor and swerve interfaces as its leaf and mid-level instances, and is candid about the open questions that remain before a team can wire it up and run it. ## Chapters **I. The unifying idea** 24. [From Elite to League](24-elite-to-league.md) — what we keep, what we change, and what "portable" buys. 25. [The Portable Component Model — the faceplate](25-portable-component-model.md) — four channels, one pure `update`, the fill-pattern that *is* the taxonomy, and a worked elevator. **J. The instances** 26. [The portable motor interface](26-portable-motor-interface.md) — the leaf component: two PODs, a `oneof` command, capability tiers, a proto3 source of truth. 27. [The portable swerve interface](27-portable-swerve-interface.md) — the mid-level component: five layers, one seam, and `ModuleIO` as two motors plus an encoder. 28. [`RobotState` and `Superstructure` as components](28-robotstate-superstructure-blocks.md) — the two higher seams recovered as instances; why a subsystem and an executive are the same kind. **K. The dividends and portability** 29. [Telemetry, replay, and tests — the dividends, at every scale](29-telemetry-replay-tests.md) — the inputs-struct idea generalized from leaves to executives. 30. [Lifecycle and graceful degradation](30-lifecycle-degradation.md) — health as state, the null component as the fault state. 31. [The ROS bridge and language portability](31-ros-bridge-portability.md) — keep the message semantics, drop the transport. **L. Maturity of the proposal** 32. [Open questions and the road to a build recipe](32-open-questions.md) — the decisions now on the record, the questions still open, and what must close before the generators emit to this contract. # 24. From Elite to League — what we keep, what we change https://frc-code-scout.jointheleague.org/part-3/24-elite-to-league/ Parts I and II reconstructed an architecture that top teams converged on without anyone designing it. Part III takes the next step: it argues that the architecture is *more uniform than it looks*, and that naming the uniformity buys things the Elite Architecture leaves on the table. This is the only prescriptive part of the book. Everything before it described what teams do; from here on we propose what we'd do, and why. A note on prerequisites: Part III assumes comfort with Parts I and II — the seams, the corpus evidence, and the vocabulary they built. It also leans on two outside idioms it does not assume you know: proto-style schemas ([ch. 26](26-portable-motor-interface.md)) and ROS 2 concepts ([ch. 31](31-ros-bridge-portability.md)). Both are introduced as they are used; passing familiarity is enough. ## What we keep The League Architecture is not a replacement. It keeps every commitment Part I earned: - **The IO seam** — vendor types confined below one interface per device, so logic runs in sim, in replay, or against a different motor brand without edits ([ch. 3](../part-1/03-the-io-seam.md)). - **Intent separated from execution** — a caller requests a goal; a coordinator owns how each mechanism reaches it ([ch. 5](../part-1/05-the-coordination-seam.md)). - **Vendor confinement** — no `com.ctre` / `com.revrobotics` / `org.photonvision` type above the IO line, ever. - **The deferred-dividend discipline** — build the seam first; collect simulation, tests, and replay as additions at a known point, never as rewrites ([ch. 7](../part-1/07-cross-cutting-practices.md)). None of that is in dispute. Part III changes exactly one thing. ## What we change The Elite Architecture, as Part I presents it, has *three differently shaped seams* — IO, state, coordination — plus a pile of subsystems, each treated as its own kind of thing. You learn the IO seam, then separately learn the state seam, then separately learn coordination, then learn how a drivetrain differs from an elevator. Four shapes, four mental models. The League claim is that **they are all the same shape, repeated at different altitudes.** A motor, a sensor, a subsystem, the world-model estimator, and the superstructure are each *the same kind of object*: something configured once, then advanced each tick by folding an incoming command together with fresh observations to update its state and emit commands for the things below it. A configured transfer function with memory. What these components share is not a base class but a **faceplate** — the same four-channel interface presented at every level ([ch. 25](25-portable-component-model.md)) — and the proposal is to build to that one contract deliberately rather than rediscovering it seam by seam. ```d2 direction: right ELITE: "Elite — four shapes" { IO: IO seam STATE: state seam COORD: coordination seam SUBS: subsystems } LEAGUE: "League — one faceplate, repeated" { EXEC: "Executive" S1: "Subsystem" S2: "Subsystem" M1: "Motor" M2: "Motor" EXEC -> S1 EXEC -> S2 S1 -> M1 S1 -> M2 } ELITE -> LEAGUE: "name the common shape" ``` The recursion is the whole idea: a subsystem is a component whose children are motors; a superstructure is a component whose children are subsystems. The same faceplate describes a leaf actuator and the robot-wide executive, and the difference between them is not a different base class — it is *which of the same four channels they populate* ([ch. 25](25-portable-component-model.md)). ## What "portable" buys The payoff of naming the shape is a single word in the part's title: *portable.* Because every component presents the same faceplate — four serializable data objects plus one pure step — three things become free **at every scale**, not just at the motor: - **Telemetry and replay** for the entire robot — snapshot every component's channels each tick and you have AdvantageKit-grade logging from leaf to executive ([ch. 29](29-telemetry-replay-tests.md)). - **Unit tests** for the entire robot — a pure `update` is testable by replaying recorded inputs, with no hardware and no scheduler ([ch. 29](29-telemetry-replay-tests.md)). - **Language and framework portability** — the faceplate maps cleanly onto a ROS 2 node, which is strong evidence the factoring is conventional rather than idiosyncratic ([ch. 31](31-ros-bridge-portability.md)). And it gives the architecture a place to fix the disciplines FRC most conspicuously skips — graceful degradation and managed lifecycle ([ch. 30](30-lifecycle-degradation.md)) — once, in the shared shape, instead of bolting them onto each subsystem. ## The scope, honestly This is a proposal, not finished doctrine. The motor and swerve interfaces ([ch. 26](26-portable-motor-interface.md), [ch. 27](27-portable-swerve-interface.md)) are worked all the way down to a proto3 schema; the higher components ([ch. 28](28-robotstate-superstructure-blocks.md)) are sketched but not yet shipped; and the model has load-bearing open questions about threading, how driver bindings become commands, and how it sits on WPILib's scheduler ([ch. 32](32-open-questions.md)). Part III states the shape, shows the instances, and is candid about what must close before a team can wire it up and run it. The thesis in one line: **a robot is a tree of components presenting one identical faceplate, and the kind of component is just which channels it populates.** The next chapter makes that precise. # 25. The Portable Component Model — the faceplate https://frc-code-scout.jointheleague.org/part-3/25-portable-component-model/ This is the core of the proposal. One shape describes every active thing on the robot, and the rest of Part III is consequences of it. ## The faceplate: four channels and one step Call each active thing on the robot a **component** — a motor, a sensor, a subsystem, the superstructure. The word stays lowercase and informal, because there is deliberately no shared supertype (see [§A discipline, not a base class](#a-discipline-not-a-base-class)). What every component shares is its **faceplate** — the fixed set of sockets it presents to the rest of the robot, the same four jacks on the front of every module no matter what circuitry sits behind them. The faceplate is four serializable data objects plus one pure step: ``` configure(Config) // once: parameters / calibration / identity (State′, Command_out[]) = update(Command_in, Observations) // each tick: fold in → advance → emit state() -> State // read the exposed state ``` - **`Config`** — what you parameterize it with: CAN IDs, gear ratios, gains, interlock tables. Mostly set once, with a defined runtime door for the parts that retune live ([§Config](#config-is-parameters)). - **`Command_in`** — the intent you send it this tick: a setpoint, a goal, a mode request. - **`State`** — what it exposes: its estimate (the measured/fused quantity) **and** its status (mode, `atGoal`, health). - **`Command_out`** — the intents it emits for the components below it. *A component's `Command_out` is literally its children's `Command_in`.* Commands are the edges between components. `Observations` is deliberately not a fifth jack on the faceplate. It is **the `State` of the component's children (and of designated peers such as `RobotState`)**, collected by the outer wiring layer and handed to `update` as its second argument. The tick's timestamp rides in `Observations` too — it is a fact about the world, delivered like any other. ```d2 direction: down CFG: "Config — parameters / calibration" ABOVE: parent CMP: "Component configure(Config) once (State′, Command_out[]) = update(Command_in, Observations)" BELOW: children CFG -> CMP: configure (once) ABOVE -> CMP: "Command_in — intent from above" CMP -> ABOVE: "State — estimate + status" { style.stroke-dash: 4 } CMP -> BELOW: "Command_out — intent to below" BELOW -> CMP: "Observations — children's State (+ timestamp)" { style.stroke-dash: 4 } ``` Naming follows the [motor spec's](26-portable-motor-interface.md) rule — a name must survive its reader — which is where `Command`/`State` are defended in full against AdvantageKit's `Inputs`/`Outputs`. ## The fill-pattern *is* the taxonomy The non-obvious result — the thing that makes this more than restating the actor model — is that **which of the faceplate's channels a component populates classifies what kind of component it is.** There is no separate type hierarchy for sensors versus actuators versus controllers; the fill-pattern — which jacks are wired, which are left empty like a chip's NC pins — is the type: | Component kind | Config | Cmd in | State | Cmd out | one line | |---|:--:|:--:|:--:|:--:|---| | **Sensor** (color, light) | ✓ | – | ✓ | – | a pure source of observations | | **Actuator / leaf** (motor) | ✓ | ✓ | ✓ | – | command in, state out, no children | | **Estimator** (`RobotState`) | ✓ | – | ✓ | – | a sensor that *fuses* | | **Subsystem** (elevator, drive) | ✓ | ✓ setpoint | ✓ | ✓ motor cmds | a controller over leaves | | **Executive** (superstructure) | ✓ | ✓ goal | ✓ | ✓ subsystem goals | a controller over subsystems | Three things fall out of the table. **A subsystem and a superstructure are the same kind** — both fill all four channels, differing only in whether their children are motors or subsystems; this is why "even the executive fits." **A sensor and an estimator share a fill-pattern and differ in what they observe** — both command channels empty, state out; the sensor's `Observations` come from hardware, while the estimator's are the `State` of designated peers (drive odometry, vision poses), which it fuses. Its `Command_in` is genuinely empty — nobody commands an estimate. And **the robot is a tree of components**: commands flow *down* (driver → executive → subsystem → motor → hardware) and state flows *up*. ```d2 direction: down DRV: Driver / Auto EXEC: "Executive (superstructure)" SUBA: Subsystem SUBB: Subsystem MOT: Motor (leaf) HW: Hardware DRV -> EXEC: goal EXEC -> SUBA: subsystem goal EXEC -> SUBB: subsystem goal SUBA -> MOT: motor command MOT -> HW: voltage HW -> MOT: "state up" { style.stroke-dash: 4 } MOT -> SUBA: "state up" { style.stroke-dash: 4 } SUBA -> EXEC: "state up" { style.stroke-dash: 4 } ``` ## Emission is a return value, never a side effect The single most important implementation rule. `update` *returns* its outgoing commands; it does not hold references to its children and push into them. - **Return-value form (do this):** a test feeds a recorded `Command_in` + `Observations` and asserts on `(State′, Command_out)` with zero hardware and no scheduler. Replay re-runs the same pure function over a log. ROS bridges it as publish-after-spin. - **Push form (don't):** the component calls `child.setControl(...)` internally — now it is coupled to its children's identities, the seam you protected is gone, and you cannot test it in isolation. So the "emit a command for something below" is the *output of a pure function*, and an **outer wiring layer** (the periodic loop, `RobotContainer`) routes each component's `Command_out` to the next component's `Command_in`. The component is ignorant of who consumes its output — exactly as a rack module doesn't know what's patched into its output jack. This is the IO-seam principle ([ch. 3](../part-1/03-the-io-seam.md)) applied recursively, up the whole tree. ## No wall-clock reads inside `update` The companion rule, with the same weight. `update` never calls `Timer.getFPGATimestamp()` — or any clock: the tick's timestamp arrives inside `Observations`, like every other fact about the world. A component that reads the clock has smuggled in a hidden input — replay would feed it the recorded commands and observations while it silently reads *now*, and the same log would produce different outputs on different days. Deltas, timeouts, debounces, and profile clocks are all computed from the observed timestamp. Time is data; treat it like the rest. ## `State` is estimate **and** status For a motor, state is just the physical measurement — its measured position *is* its state variable. Above the leaf, state splits in two: the **estimate** (the measured or fused quantity) and the **status** (what the component is doing — its mode, `atSetpoint`, fault flags). For a subsystem you need `atGoal`; for an executive the status (which mode, is it interlocked, is it ready) is the *primary* output and the estimate is secondary. So `State` carries `{ estimate, status }`. This is also why every level is named `…State` (`MotorState`, `RobotState`): naming device, subsystem, and world state the same reveals they are the same kind of thing at different scales. ## `State` versus internal memory `State` is what a component *exposes* on its faceplate, not everything it remembers behind it. A component may keep internal memory — a PID integrator, motion-profile progress, a debounce timer — that never appears in its `State`, provided `update` stays deterministic: the same `Command_in`, `Observations`, and internal history must always produce the same outputs. The consequence, stated honestly: replay is guaranteed bit-identical only when re-run **from tick 0 of a complete log with deterministic code** — which is exactly AdvantageKit's actual model. Re-entering a log mid-stream would require snapshotting every component's internal memory each tick, and we deliberately do not require that. ## Config is parameters {#config-is-parameters} `Config` is identity and calibration that does not change within a control session — kept as its own channel, separate from the per-tick `Command`, so slow structural change never pollutes the command log. Most of it is write-once; a defined subset is runtime-settable (PID gains, current limits, vision trust) through a `reconfigure(partialConfig)` door. The boundary test: *if it changes every loop it's a `Command`; if it identifies or calibrates the component across a session it's `Config`.* ## A discipline, not a base class {#a-discipline-not-a-base-class} The failure mode of every "universal component interface" is the inner-platform effect: `Component` with four Java generics metastasizes through every signature and constrains nothing, because an interface that fits a color sensor *and* a superstructure necessarily says almost nothing. So the model is delivered as a **contract you follow**, not a superclass you extend: > Every component takes a `Config` POD, accepts a `Command` POD, exposes a `State` POD (estimate + > status), advances via one pure `update` that *returns* its outgoing commands, and obeys the > lifecycle. Each is its own concrete types; there is no shared supertype carrying them. Followed consistently, that convention buys uniform logging, replay, sim-testing, and ROS-bridging at every scale — which is the entire point — without a lowest-common-denominator interface. This is also why *faceplate* is a word of the book's vocabulary and never a name in the code: the concrete types keep their natural names (`ElevatorCommand`, `MotorState`, `ElevatorIO`), and the faceplate is the shape they all share. Appendix B records the naming decision in full — including why the earlier working name, *block*, lost. The leaf hardware adapters keep their established `…IO` suffix — an `…IO` is the downward edge of a leaf component, not a competing concept. Why trust this shape? Because it is simultaneously a **ROS 2 lifecycle node** (parameters + subscriptions + publications + managed states) and a **Simulink block** (parameters + ports + internal state, composed by wiring ports) — the Hewitt actor is at best a distant cousin, since actors are asynchronous and never synchronously return their outputs. When one structure is independently arrived at by battle-tested communities, it is load-bearing — and we get to steal their refinements rather than rediscover them. The next chapters do exactly that. ## The contract, worked once: an elevator Before the instances, here is the whole contract in one place — a single elevator component, small enough to read in a minute. This is *illustrative of the contract, not a finished library*: real code would carry more fields, more status, and the lifecycle. First the three PODs: ```java record ElevatorConfig(double gearRatio, double drumRadiusM, double maxVelMps, double maxAccelMps2, double kP, double kG) {} record ElevatorCommand(double heightM) {} // Command_in: one goal record ElevatorState(double heightM, double velMps, // estimate boolean atGoal, boolean connected) {} // status record ElevatorObs(double timestampS, MotorState motor) {} // children's State + the tick's time record ElevatorTick(ElevatorState state, // what update returns List commandsOut) {} ``` Then the pure step — a profiled setpoint, no clock, no hardware, emission as the return value: ```java ElevatorTick update(ElevatorCommand cmd, ElevatorObs obs) { double dt = obs.timestampS() - lastTs; // time is an observation lastTs = obs.timestampS(); // internal memory, not State setpoint = profile.calculate(dt, setpoint, // TrapezoidProfile — pure math new TrapezoidProfile.State(cmd.heightM(), 0.0)); double height = obs.motor().positionRad() * cfg.drumRadiusM() / cfg.gearRatio(); double velMps = obs.motor().velocityRadS() * cfg.drumRadiusM() / cfg.gearRatio(); double volts = cfg.kP() * (setpoint.position - height) + cfg.kG(); var state = new ElevatorState(height, velMps, Math.abs(cmd.heightM() - height) < 0.02, // atGoal obs.motor().connected()); return new ElevatorTick(state, List.of(MotorCommand.voltage(volts))); // emission is the return value } ``` And the thin impure shell — a WPILib `Subsystem` whose `periodic()` is the wiring layer: ```java public class Elevator extends SubsystemBase { private final ElevatorLogic logic = new ElevatorLogic(CONFIG); private final MotorIO io; // vendor types live below this line private ElevatorCommand cmd = new ElevatorCommand(0.0); public void setGoal(double heightM) { cmd = new ElevatorCommand(heightM); } @Override public void periodic() { var obs = new ElevatorObs(Timer.getFPGATimestamp(), io.read()); // 1. read var tick = logic.update(cmd, obs); // 2. pure step io.apply(tick.commandsOut().get(0)); // 3. actuate // 4. log cmd, obs, tick.state(), tick.commandsOut() — all PODs } } ``` Everything the chapter argued is visible in these forty lines: the timestamp arrives inside `ElevatorObs` rather than from a clock; `update` touches no hardware and returns its command instead of pushing it; and the only impure code is the shell that reads, steps, and applies. A test constructs an `ElevatorObs` by hand and asserts on the returned tick — no scheduler, no HAL. Chapters 26–28 work this same contract at the leaf, the drivetrain, and the executive, starting with [the portable motor interface](26-portable-motor-interface.md). # 26. The portable motor interface — the leaf component https://frc-code-scout.jointheleague.org/part-3/26-portable-motor-interface/ The motor is the leaf of the component tree: its faceplate carries `Config` (CAN id, gains), `Command` (u), `MotorState` (x), and **no** outgoing-command channel. It is where the model touches metal, so it is where the model is worked all the way down to a schema. Part II surveyed the six `MotorIO` shapes the corpus actually uses ([ch. 17](../part-2/17-motor-interfaces.md)); this chapter takes the durable ideas in them and recasts them as **two serializable data objects plus a capability-tiered port**, defined once in proto3 and regenerable into any language — so the design choices show up as choices. ## Two data objects, named for what they are A motor interface carries data in two directions, and each is a plain serializable object — not a bag of method calls — so both can be logged, diffed, replayed, and sent over a wire: - **`Command`** — what you're telling the motor to do. The intent. (A leaf has no outgoing-command channel, so the `_in` suffix is dropped: a motor's `Command_in` is just `Command`.) - **`MotorState`** — what the motor is currently doing. Its physical state. The dominant FRC convention (AdvantageKit) names the read struct `XxxIOInputs` — and has no symmetric `Outputs` POD at all: the write side is imperative setters, and output logging is ad-hoc `recordOutput(...)` calls. We reject the `Inputs` naming, and the critique carries either way. That name is *relational, not identity*: "input" only means something once you say "input to what," and it inverts under viewpoint — from the code's chair a sensor reading is an input; from control theory's chair the *command* is the input (`u`) and the reading is the output. The rule we adopt instead, used throughout Part III: **a name must survive a change of reader.** `Command` and `MotorState` do — a command is a command from any viewpoint — and they are exactly the state-space pair `u`/`x`. `MotorState` is honest for a motor specifically: a motor's measured position and velocity *are* its state variables, so "state" is accurate here in a way it is not for a whole robot, where hidden state must be *estimated* (which is what `RobotState`, [ch. 28](28-robotstate-superstructure-blocks.md), is for). ## The schema: a `oneof` command, a flat state The data is the hard, consistency-critical part; proto3 is the source of truth. The command is a tagged union over control modes — `oneof` structurally enforces *at most* one mode at a time, so you *cannot* set voltage and position together; an unset `oneof` is still valid on the wire, so the boundary validates that `control` is set before any command is applied — plus optional modifiers: ```proto message Command { oneof control { // the discriminant — at most one; boundary validates it's set double duty_cycle = 1; // [-1, 1] double voltage = 2 [(unit) = "V"]; double torque_current = 3 [(unit) = "A"]; double position = 4; // PID-to-position double profiled_position = 5; // Motion Magic / trapezoid double velocity = 6; Neutral neutral = 7; // BRAKE / COAST } optional uint32 slot = 8; // modifiers — null = "use default / don't override" optional double feedforward_voltage = 9 [(unit) = "V"]; optional double max_stator_current = 10 [(unit) = "A"]; // transient cap for this command } message MotorState { bool connected = 1; // discriminant — always present optional double position = 2; // rad or m (null = "not reported this tick") optional double velocity = 3; optional double applied_voltage = 6 [(unit) = "V"]; optional double stator_current = 8 [(unit) = "A"]; optional double temperature = 11 [(unit) = "degC"]; optional bool hardware_fault = 15; // … electrical, thermal, controller-introspection, and limit fields elided — as is the // declaration of the (unit) FieldOptions extension the annotations above require … } ``` Two rules carry the schema. **Nullable payloads, non-null discriminants:** absence must be distinguishable from a real value, and `0.0` is a real reading while `NaN` is fragile — so payloads are `optional` while the discriminants (`Command.control`, `MotorState.connected`) are always present. In `MotorState`, null means "unknown / not reported by this device this tick"; in `Command`, null means "use the configured default / don't override." And **names carry their own meaning:** a field is named for the *quantity* (`stator_current`), never the unit (`stator_amps`) — the unit is machine-readable metadata — and every bound is `max_`/`min_`-prefixed, so a bare `stator_current` is unambiguously a *reading*, never a limit. ## One message set for the whole capability spectrum Real motors form a spectrum — PWM-only (set a duty cycle, nothing comes back), PWM-plus-encoder (open-loop command, but position readable), smart controller (onboard closed loop, gains, motion profiling). Rather than one fat interface full of `unsupported()` stubs or N forked message types, capability factors onto **two independent axes** while the messages stay unified: | Axis | What upgrades it | How it's expressed | |---|---|---| | **Command** — which control modes are accepted | an onboard controller | typed port tiers (`BasicMotor` → `SmartMotor`) + declared `Capabilities.command_modes` | | **Observation** — which state fields come back | a sensor / encoder | populated `MotorState` fields + `Capabilities.state_fields` | The axes are orthogonal — an encoder upgrades observation without touching command — and the nullable `Command`/`MotorState` already express both: a PWM motor's `Command` only ever sets `duty_cycle`, and its `MotorState` populates only `connected`. So there is exactly **one `Command` and one `MotorState` on the wire** (uniform translation, logging, replay), plus a **declared `Capabilities`** per motor and **generated typed port tiers** for compile-time ergonomics: ```text interface Motor { // universal data plane — every motor, capability-validated void apply(Command cmd); // the ONLY `apply`: rejects modes not in capabilities() MotorState read(); // once per tick; fields per capabilities() Capabilities capabilities(); } interface BasicMotor : Motor { // PWM-class: open-loop only void setDutyCycle(double pct); void setVoltage(double v); void setNeutral(Neutral m); } interface SmartMotor : BasicMotor { // onboard closed loop + config void setPosition(double units); void setVelocity(double ups); void resetPosition(double units); void setGains(uint32 slot, Gains g); void setCurrentLimits(CurrentLimits c); } ``` `apply(Command)` is the data-plane entry — where ROS-originated, replayed, and logged commands enter — and it validates the mode against `capabilities()` at runtime. The `set…` helpers give hand-written robot code compile-time guidance: a `BasicMotor` reference simply *has no* `setPosition`. The port is not an RPC service; `apply`/`read` are in-process calls, and ROS is reached by translation, not by making this a gRPC endpoint. This is the [capability-typed-devices pattern](../part-1/08-alternatives.md) — interfaces named by capability, not vendor — reconciled with a single message schema. Where the purity boundary sits deserves one explicit paragraph, because the component's `update` never touches this port. The `…IO` adapter — the object that owns the vendor handle — is the **impure shell**: its `read()` samples hardware into a `MotorState`, its `apply(Command)` pushes a command out to metal. The wiring layer calls `read() → update() → apply()` each tick, in that order, and everything between the two IO calls is pure ([ch. 25](25-portable-component-model.md)). The component computes; the shell touches the world. ## Units and nullability, settled by codegen Units follow ROS **REP-103**: everything SI by convention (m, rad, m/s, V, A, °C), bare `double`, with the unit declared in the schema as metadata (`[(unit) = "rad/s"]`). That makes the corpus's raw-doubles-versus-`Measure`-types debate a *codegen choice* — from the same annotation, emit raw `double` accessors for the hot path or typed `Measure` accessors where ergonomics win — and the wire form stays SI doubles, so translation to ROS is identity on the numbers. Nullability gets one idiom per language (`Optional` in Java, `None` in Python, `Option` in Rust); `NaN` is **not** an in-code value — it exists only as a wire encoding on the ROS side, converted once at the boundary, so no application code ever sees both. One allocation decision is settled here because it constrains every generated binding: **the in-loop channel types are plain mutable records/structs, and proto3 is the schema source of truth that appears only at the log-and-wire boundary.** Generated protobuf-java messages are immutable, builder-allocating objects — constructing them every 20 ms tick on a two-core roboRIO is a steady garbage-collection tax, which is why WPILib itself serializes with QuickBuffers rather than protobuf-java. So the hot loop passes reusable in-memory types generated *from* the schema, and the protobuf encoding is produced only when a tick is logged or crosses the wire ([ch. 31](31-ros-bridge-portability.md)). ## Crossing to ROS, both directions Because the proto is kept structurally isomorphic to the ROS target messages, the bridge is a mechanical field map, not hand-tuned logic, and it needs exactly two conventions: | Concern | Mapping | |---|---| | geometry, units, field names | **identity** — mirrored from `geometry_msgs`, both REP-103 SI | | nullability | `None` ↔ `NaN` (ROS's own "no data" idiom) | | `Command` `oneof` | ↔ `{ uint8 mode, double value }` — the `oneof` case ↔ the mode constant, lossless | Commands cross from ROS too, and they are safe by construction: an arriving `Command` is validated against `capabilities()` — a `POSITION` command to a PWM-only motor is rejected or clamped, exactly as `ros2_control` refuses to write an interface a hardware component never exported. The leaf is now a clean component. The next chapter composes four pairs of these into a drivetrain: [the portable swerve interface](27-portable-swerve-interface.md). # 27. The portable swerve interface — the mid-level component https://frc-code-scout.jointheleague.org/part-3/27-portable-swerve-interface/ A drive subsystem is the first interesting component above the leaf: its children are four module components, each two motors. Its `Command_in` is a control-intent union, its `State` is the fused drive state, and its `Command_out` is four module setpoints. Working it out shows seam-granularity for what it is — *the altitude at which you draw a component boundary* — and reuses the [motor interface](26-portable-motor-interface.md) wholesale. Part II built the swerve subsystem from the corpus ([ch. 19](../part-2/19-the-drivetrain-subsystem.md)); this chapter gives it the portable contract. ## They already agree Put the field's four reference swerve systems side by side — CTRE Phoenix 6, Team 6328's AdvantageKit template, YAGSL, and WPILib's math — and the apparent diversity collapses. Every one computes module setpoints with the **same WPILib types** (`SwerveDriveKinematics`, `SwerveModuleState`, `ChassisSpeeds`, `SwerveDrivePoseEstimator`). That is the shared substrate. They differ on exactly **two** decisions: *where the hardware seam goes* and *whether there is a control-intent vocabulary.* So the design space is not four architectures; it is one architecture with two open choices — and the field has already produced the best answer to each. The right swerve is **"AdvantageKit's seam + CTRE's vocabulary, on WPILib's math."** ## Five layers, one seam ```d2 direction: up L0: "L0 — math substrate: WPILib SwerveModuleState / Position · ChassisSpeeds · Kinematics · PoseEstimator" L1: "L1 — hardware seam: ModuleIO + GyroIO, inputs struct ◄ THE seam — vendor types ONLY here" L2: "L2 — module logic: optimize() · open/closed-loop selection · drive feedforward (no vendor types)" L3: "L3 — drive coordination: kinematics · pose estimator · signal-synced odometry thread · (optional) setpoint generator" L4: "L4 — control vocabulary: SwerveRequest union (FieldCentric · FacingAngle · Brake · …)" L0 -> L1 -> L2 -> L3 -> L4 L1.style.fill: "#1f3a5a" L1.style.font-color: "#ffffff" ``` Two load-bearing rules, inherited from the rubric. **Vendor SDK types appear only at L1** — everything L2 and above is WPILib-and-our-own-types only (the D1 rule applied to swerve). And **the seam is a data struct, not a method bag** — L1's read side is a serializable inputs struct, which makes it simultaneously the replay seam, the sim-mock seam, and the vendor-swap seam: one cut buys all three. ## L1 is just two motors and an encoder The seam to adopt is AdvantageKit's `ModuleIO`/`GyroIO` — minimal and field-proven: a per-module read struct plus four write verbs (drive open-loop, drive velocity, steer open-loop, steer position), and a gyro read struct. But the clean formulation is not a fresh interface. Those four verbs are exactly **two motors' worth of the motor spec** plus an absolute encoder: ``` ModuleIO ≙ { drive: Motor (velocity tier), steer: Motor (position tier), azimuth: AbsoluteEncoder } ``` So the swerve component inherits the motor spec's nullable payloads, capability tiers, and ROS translation for free — a steer motor is "a `Motor` whose `Command` is a position `oneof`," nothing new to design. The flat five-method `ModuleIO` is a convenience facade over that canonical pair. The one leak to legislate against: AdvantageKit's template reuses CTRE's `SwerveModuleConstants` as a constants bag above the seam, because that is what the Tuner X generator emits. We disallow that — constants cross as our own neutral record (`ModuleConstants{ driveGearRatio, wheelRadiusMeters, locationMeters, encoderOffsetRot, … }`), populated *from* `TunerConstants` by the L1 adapter, never referenced by type above it. This is the swerve form of *generate the constants, own the architecture.* ## Two altitudes for the seam Where you cut L1 depends on what sits below it, and the corpus shows both in live elite use: - **Per-module** — `ModuleIO` + `GyroIO`, the subsystem owning kinematics and odometry. Correct when you **build from motors**: the vendor abstraction stops at one drive + one steer + one encoder, four times over. - **Per-drivetrain** — a single `DriveIO` wrapping an *entire vendor swerve* (CTRE's `CommandSwerveDrivetrain`), with the contract **`SwerveRequest` in, `SwerveDriveState` out.** Correct when you **inherit a vendor's swerve**, because CTRE already abstracts the four modules internally. This is the 254/2910 pattern: `CommandSwerveDrivetrain` is demoted to a plain device (it does *not* `implements Subsystem`) and a hand-owned `DriveSubsystem` sits on the seam. Both are the same seam — data-struct read side, intent write side, vendor below only — cut at different heights. **Rule of thumb: own the motors → per-module; inherit a vendor swerve → per-drivetrain.** ## L4: control is an intent object AdvantageKit stops at `runVelocity(ChassisSpeeds)`; CTRE's `SwerveRequest` is the better idea worth lifting — control is an intent object you hand the drivetrain each loop, not a method you call — recast off vendor types as a tagged union, so requests are loggable and replayable like everything else. The arms are not speculative; the counts are corpus reference frequencies across 683 repos (the full cloned corpus — see [Appendix A](../appendices/how-we-developed-this/)), so the union is the measured ~90% of real usage: ``` SwerveRequest = oneof { FieldCentric { vx, vy, omega } // field frame, REP-103 [324×] FacingAngle { vx, vy; Rotation2d heading } // translate + θ-PID heading [115×] ApplyChassisSpeeds { ChassisSpeeds; bool fieldRel} // path-follower entry [127×] RobotCentric { vx, vy, omega } // [80×] Brake { } // X-lock the wheels [119×] PointWheelsAt { Rotation2d angle } // [74×] Characterize { CharMode mode; double value } // plant-response test [~310×] } modifiers: DriveRequestType drive = OPEN_LOOP | VELOCITY // lands on the two ModuleIO drive verbs wheelForceFeedforwardsX/Y // the L3 setpoint generator's per-module forces ``` (One vintage note: Phoenix 6's 2025 release split `ApplyChassisSpeeds` into `ApplyRobotSpeeds` and `ApplyFieldSpeeds`; the counts above use the older class name because most of the corpus predates the split.) `DriveRequestType` *is* the whole open-vs-closed-loop switch, and it maps straight onto the two `ModuleIO` drive verbs — no extra machinery. One naming decision is deliberate: the characterization arm is **`Characterize`**, not CTRE's `SysId`. "System identification" collides head-on with software's prior claims on both words — `getSysId()` in any other codebase returns a string handle, not a feedforward routine — so it fails [ch. 26](26-portable-motor-interface.md)'s survive-a-change-of-reader rule. The general policy: *import a control term only if the destination domain hasn't already spent the word.* `plant`, `chassis speeds`, `kinematics`, `pose` import cleanly; `system identification` and bare `observer` do not. ## L3: state out, and the optional planner State out is one immutable snapshot, modeled field-for-field on CTRE's `SwerveDriveState` — which is itself a flat public-field POD, meaning CTRE independently arrived at the inputs-struct-as-data idea, just without the replay seam around it. The field that matters: **`Pose` is read 682 times across the corpus, more than any actuator field on any subsystem** — the empirical proof that the drivetrain is the world-model anchor ([ch. 6](../part-1/06-the-drivetrain.md)). Its state snapshot, not its motion command, is its primary output. Odometry **must be signal-synced** — a 250 Hz (CAN FD) / 100 Hz (RIO) thread that samples drive, steer, and gyro signals together and timestamps them — because timestamped high-rate samples are what make vision fusion and replay correct; a bare 50 Hz `Notifier` (YAGSL) is not enough. The thread is a property of the L1 adapter (it knows the vendor's signal API) and publishes into the inputs struct, so L3 stays vendor-neutral. Above the modules sits the optional **`SwerveSetpointGenerator`**, a dynamic-feasibility filter (pure WPILib-and-`DCMotor` math, no vendor types) that backs a desired `ChassisSpeeds` off to one actually reachable this loop without slipping a wheel; prefer PathPlannerLib's torque/friction model, and let its per-module force feedforwards flow down as the arbitrary-feedforward field of the L1 drive command — closing the loop with the motor spec again. A team climbs capability tiers — drive-only → fused localization → feasibility-planned — over the *same L1*, never re-cutting the seam. And the whole stack crosses to ROS cleanly: a swerve drive is a `Twist`-in / `Odometry`-out component with four `JointState` pairs underneath — the shape a `ros2_control` swerve controller takes (cf. `diff_drive_controller`; swerve controllers exist as third-party packages) ([ch. 31](31-ros-bridge-portability.md)). With the leaf and mid-level components in hand, the next chapter recovers the two higher seams as components: [`RobotState` and `Superstructure`](28-robotstate-superstructure-blocks.md). # 28. RobotState and Superstructure as components https://frc-code-scout.jointheleague.org/part-3/28-robotstate-superstructure-blocks/ The motor and swerve chapters recovered the bottom two layers as components. This chapter does the same for the two *higher* seams of Part I — the world model and the coordinator — and in doing so collects the payoff promised in [ch. 24](24-elite-to-league.md): the seams that looked like three different shapes turn out to be the same faceplate at different altitudes. ## `RobotState` is an estimator The state seam ([ch. 4](../part-1/04-the-state-seam.md)) is a component with an unusual fill-pattern: it is **a sensor that fuses.** Its channels: - **`Config`** — the vision standard deviations and trust weights. - **`Command_in`** — empty. Nobody commands an estimate; what flows *into* this component — odometry from the drive, timestamped pose measurements from vision — arrives via the `Observations` channel, as the `State` of its designated peers ([ch. 25](25-portable-component-model.md)). - **`State`** — the fused `Pose2d` plus confidence (and, at the elite end, game-piece and mechanism state). - **`Command_out`** — none. It commands nothing; it only emits an estimate. ```d2 direction: right DR: Drive subsystem VIS: Vision subsystem RS: "RobotState (estimator) Config: vision std-devs update(observations) → fused estimate" CONS: "Consumers: Auto · Path · Aim · Superstructure" DR -> RS: "odometry (observations)" { style.stroke-dash: 4 } VIS -> RS: "vision poses (observations)" { style.stroke-dash: 4 } RS -> CONS: "State (estimate)" ``` In control terms it is an **observer** — the component that infers hidden state from measurements — which is exactly why pose estimation belongs in its own component rather than privately inside the drive subsystem. The mechanics (the time-interpolating buffer, the fusion math) are Part II ([ch. 20](../part-2/20-the-world-model.md)); the point here is structural: both command channels are empty — everything it consumes arrives as `Observations`, and it emits nothing but `State` — and that emptiness is what distinguishes an estimator from a controller. There is one honesty note, which [ch. 32](32-open-questions.md) returns to. `RobotState` does not sit in the command tree — nobody *commands* it and it commands nothing. It is a **cross-cutting peer**, a shared blackboard that many components read. Commands form a tree; state, with `RobotState` as a hub, forms a DAG. Same faceplate, different wiring. ## `Superstructure` is an executive The coordination seam ([ch. 5](../part-1/05-the-coordination-seam.md)) is a component that fills **all four** channels: - **`Config`** — the interlock table and the goal graph. - **`Command_in`** — one robot-wide goal from the driver or an autonomous routine. - **`State`** — its FSM mode and readiness flags (here the *status* half of state is the primary output; the estimate is secondary). - **`Command_out`** — a per-subsystem goal for each mechanism. ```d2 direction: down DRV: Driver / Auto SUP: "Superstructure (executive) Config: interlock table, goal graph update(goal, observations) → subsystem goals" S1: Elevator subsystem S2: Arm subsystem S3: Intake subsystem RS: RobotState DRV -> SUP: goal SUP -> S1: setpoint SUP -> S2: setpoint SUP -> S3: setpoint RS -> SUP: "pose / situation (observations)" { style.stroke-dash: 4 } ``` It is a controller whose plant is *other subsystems* instead of motors. The guarded transition function that turns one goal into a legal sequence of setpoints — and holds the interlocks in one place — is just this component's `update`. ## Why a subsystem and an executive are the same kind Set the two side by side and the recursion is plain. A **subsystem** fills all four channels and its `Command_out` feeds *motors*. A **superstructure** fills all four channels and its `Command_out` feeds *subsystems*. Nothing else differs. The executive is not a special top-level construct; it is a component whose children happen to be other components. This is what lets the model claim "even the coordinator fits": the same interfaces carry intent downward and state upward at every altitude. It is also why every level's exposed POD is named `…State`, and why above the leaf that `State` splits into an estimate half and a status half — the argument is [ch. 25's](25-portable-component-model.md), and these two components are its extreme instances: `RobotState` is nearly all estimate, the superstructure's state nearly all status. | Component | `Config` | `Command_in` | `State` | `Command_out` | |---|---|---|---|---| | **Motor** (leaf) | CAN id, gains | voltage/velocity/position | `MotorState` | — | | **Swerve module** | gear, radius, offset | module setpoint | measured state | 2× motor command | | **Drive subsystem** | track geometry | `SwerveRequest` | `SwerveDriveState` | 4× module setpoints | | **`RobotState`** | vision std-devs | — | fused pose + confidence | — | | **Superstructure** | interlock table | driver/auto goal | mode + readiness | per-subsystem goals | (`RobotState`'s row has an empty `Command_in` by design — odometry and vision poses reach it as `Observations`, not as commands.) Read the table top to bottom and it is one shape applied at five altitudes — the genus ([ch. 25](25-portable-component-model.md)) and its species. The deep dives for these two seams are Part II ([the world model](../part-2/20-the-world-model.md); coordination in [ch. 22](../part-2/22-coordination-state-machines.md) and [ch. 23](../part-2/23-coordination-graphs-trees.md)); what Part III adds is that they are not separate inventions but the same faceplate. The next three chapters collect what that uniformity buys: [telemetry, replay, and tests for free](29-telemetry-replay-tests.md). # 29. Telemetry, replay, and tests — the dividends, at every scale https://frc-code-scout.jointheleague.org/part-3/29-telemetry-replay-tests/ This is the chapter that pays for the whole proposal. Part I's cross-cutting practices ([ch. 7](../part-1/07-cross-cutting-practices.md)) hang off the IO seam at the *leaf* — you get simulation, replay, and tests for a motor because the motor sits behind a data-struct interface. The component model's payoff is that the *same* dividends now apply at **every altitude**, because every component — motor, subsystem, estimator, executive — presents the same four serializable PODs plus one pure step. ## Why "data, not method calls" is load-bearing Everything here rests on all four channels being **plain serializable data objects** rather than ad-hoc method calls. The Elite Architecture already learned this once: every team that builds an IO interface also builds the logged `Inputs` struct, with no exceptions ([ch. 3](../part-1/03-the-io-seam.md); [ch. 16](../part-2/16-hardware-abstraction.md) surveys the corpus's variants). The component model takes that one idea — *the seam is data* — and generalizes it from the leaf to the executive. Because `Config`, `Command_in`, `State`, and `Command_out` are all data, three capabilities fall out at once. ## Telemetry and replay, for the whole robot Snapshot every component's four PODs each tick and you have a complete, structured record of the robot — not just the motors. The drive subsystem's `SwerveRequest` in and `SwerveDriveState` out, the superstructure's goal in and per-subsystem goals out, `RobotState`'s observations in and fused pose out: all of it is captured by the same mechanism, because it is all the same shape. ```d2 direction: down TICK: "each 20 ms tick" LOG: "log: Config · Command_in · State · Command_out for every component, leaf to executive" TEL: "Telemetry — every channel at every altitude" REP: "Replay — re-run pure update() over the log" TICK -> LOG LOG -> TEL LOG -> REP ``` This is **AdvantageKit-grade replay and telemetry for the entire robot at every scale**, for the price of the determinism discipline below. Replay re-runs the recorded `Command_in` + `Observations` through each component's pure `update` and — provided the code is deterministic and the replay starts from tick 0 of a complete log ([ch. 25](25-portable-component-model.md)) — gets bit-identical `State` and `Command_out` back, so a match that misbehaved can be re-examined offline, at the executive level, not just at the motor. The Elite Architecture collects this dividend only at the leaf because only the leaf has a data seam; the component model collects it everywhere because every seam is a data seam. Replay and telemetry want different slices of that record, and it is worth separating them. **Replay needs only the boundary**: the leaf observations (what the hardware said) and the top-level commands (what the driver or auto asked) — everything in between is recomputed by the pure `update`s. **Telemetry wants the full four-channel snapshot** of every component: logging each intermediate `Command_out` is what lets you diff a replayed tick against the recorded one to catch nondeterminism, and what makes the dashboard useful — verification and visibility, not a replay requirement. And the bit-identical guarantee holds only under a short determinism checklist: no wall-clock reads inside `update` ([ch. 25](25-portable-component-model.md)), no unlogged randomness, and no dependence on unordered iteration — a `HashMap` walk that varies run to run is enough to break it. ## Tests, for the whole robot Because `update` is a **pure function over PODs**, any component is unit-testable by feeding recorded inputs and asserting on outputs — no hardware, no scheduler, no HAL: - A **motor**: feed a `Command`, assert the `MotorState` the sim model produces. - A **subsystem**: feed a setpoint and the children's `State`, assert the emitted motor commands and `atGoal`. - The **superstructure**: feed `SCORE_L4` and a `RobotState` snapshot, assert it emits the legal *sequence* of subsystem goals and refuses the illegal ones — the interlock logic, tested in isolation, with zero hardware. That last one is the prize. The coordinator holds the safety-critical sequencing of the robot, and in the Elite Architecture it is among the least-tested code because there is no clean way to drive it without the whole robot. As a pure component it is *the* most testable object: its entire contract is `(State′, Command_out) = update(goal, observations)`, and a test is literally three lines: ```java var sup = new Superstructure(CONFIG); var tick = sup.update(Goal.SCORE_L4, obsWithElevatorDown()); // hand-built or recorded assertEquals(List.of(ElevatorGoal.RAISE_TO_L4), tick.commandsOut()); // not the arm swing yet ``` This is the same move that makes `RobotState` the most unit-testable class on the robot ([ch. 4](../part-1/04-the-state-seam.md)) — generalized to every controller above it. ## The discipline that makes it true The dividend is real only if one rule holds: **emission is a return value, never a side effect** ([ch. 25](25-portable-component-model.md)). The moment a component reaches into a child and calls `child.setControl(...)`, it is no longer a pure function — you cannot replay it, and a test must stand up the child to observe what happened. So the testability and the replayability are not separate features to be added; they are the *same property* (purity) viewed two ways, and they are bought by the one discipline the model insists on. Build the components as pure transforms and you do not "add" logging or tests later — they are already there, waiting to be collected, at every altitude. The corpus truth from Part I was that almost everyone builds the leaf seam and almost no one collects the test and replay dividend even there. The component model's wager is that making the *whole robot* the same shape changes the economics: when one logging harness and one test pattern cover motor through executive, the dividend is cheap enough that teams finally take it. The next chapter adds a capability the shape makes room for but Part I never had — [lifecycle and graceful degradation](30-lifecycle-degradation.md) — and the chapter after it tests the shape against the broader field's component model. # 30. Lifecycle and graceful degradation https://frc-code-scout.jointheleague.org/part-3/30-lifecycle-degradation/ The [Lessons from Outside](../appendices/lessons-from-outside/01-lessons-from-outside.md) survey names graceful degradation as the discipline FRC most conspicuously skips: a wire comes loose, a CAN device drops, and baseline robot code either crashes or sails on commanding a motor that isn't there. ROS, Nav2, and the autonomous-driving stack treat managed lifecycle and health as table stakes. The component model is the right place to fix this once — in the shared shape — instead of bolting a special case onto each subsystem. ## A real component has a lifecycle A component is not just "construct it and call `update` forever." Modeled on ROS 2 managed nodes — with two deliberate deviations, named below — it moves through defined states: ```d2 direction: right C: constructed CF: configured E: enabled R: running D: disabled F: fault / degraded C -> CF: configure(Config) CF -> E: enable E -> R: tick R -> D: disable D -> E: re-enable R -> F: device lost F -> R: recovered ``` The deviations are improvements for FRC, not inheritance. ROS 2's primary states are Unconfigured / Inactive / Active / Finalized, and errors route through a *transitional* `ErrorProcessing` state — there is no persistent fault or degraded primary state. We add one, because an FRC mechanism can lose a device mid-match and must keep ticking in that condition for minutes, not merely transition through it. And we split ROS 2's single Active into `enabled` / `running`, matching the field-management reality that a robot is powered and configured well before it is allowed to move. Within the fault box, `fault` means total loss — the device is gone, emit safe zeros — while `degraded` means impaired but still acting: one motor of a pair lost, a reduced current budget, still pursuing its command. The transitions are the same for every component, so the discipline is written once and inherited by motor, subsystem, estimator, and executive alike. A component that is `configured` but not `enabled` holds its parameters but commands nothing; a component in `fault` accepts commands but emits only safe ones. None of this is mechanism-specific. ## Health is a field, not an exception The first concrete requirement: **a `connected` / health field lives in `State.status`, not in an exception path.** The corpus already does this at the leaf without naming it — the swerve `ModuleIO` inputs carry `driveConnected` and `turnConnected`, and CTRE's drive state carries `FailedDaqs`. The component model promotes that from a leaf convention to the universal rule: every component reports its health *as part of the state it already exposes*. A parent reads a child's `State.status` the same way it reads the estimate, so "the elevator's left motor is disconnected" propagates up the tree as ordinary state, available to the superstructure's interlock logic, the telemetry log, and the driver dashboard through one path — not as an exception that some layer must remember to catch. ## The null component *is* the fault state The second requirement makes degradation structural rather than ad-hoc. The Elite Architecture already has the right object: the `*IONull` null-object ([ch. 16](../part-2/16-hardware-abstraction.md)), a do-nothing implementation whose methods are deliberately empty so a subsystem with disconnected hardware runs as a safe no-op instead of crashing. In the component model, **the null implementation is the component in its `fault` lifecycle state**: it accepts commands, emits safe/zero outgoing commands, and reports `connected = false`. That reframing matters. Degradation stops being a special code path bolted on after the fact and becomes **a lifecycle transition of the standard shape**: a component detects a lost device, transitions `running → fault`, and its behavior in that state is simply "be the null component." Because the null component satisfies the same contract as the real one, nothing above it needs a branch for the degraded case — the executive keeps issuing goals, the failed component keeps reporting `connected = false` and emitting zeros, and the rest of the robot keeps running. A robot that loses a non-critical mechanism mid-match degrades to operating without it instead of faulting the whole program. ## Why the shape is the right home for it Graceful degradation is hard to retrofit precisely because, in a baseline architecture, every subsystem handles failure its own way — or doesn't. By giving every active thing the same faceplate, the same lifecycle, and the same `State.status` health field, the model gives degradation *one* definition that every component inherits. This is the structural hook the outside-robotics survey asked for: not a library to import, but a property of the shared shape. The discipline FRC skips becomes the default, because there is nowhere else for it to live. The shape also has one more thing to prove — that it is genuinely the same factoring the broader field uses, not a coincidence. The next chapter cashes that in: [the ROS bridge and language portability](31-ros-bridge-portability.md). # 31. The ROS bridge and language portability https://frc-code-scout.jointheleague.org/part-3/31-ros-bridge-portability/ The last argument for the component model is external. If the four-channel shape is a sound factoring of a robot component, it should map cleanly onto the broader field's component model — the ROS 2 node. It largely does, and the mapping is strong evidence that the factoring is conventional rather than idiosyncratic — not a proof, but a capability to use. ## A component is a ROS 2 node Each faceplate channel has a ROS 2 counterpart, one-for-one: | Faceplate channel | ROS 2 | |---|---| | `Config` (write-once + runtime door) | node **parameters** (+ `set_parameters`) | | `Command_in` (setpoint) | a subscribed **topic**, or a **goal** for a long-running action | | `State` (estimate + status) | a published **topic** (estimate) + the action **feedback/result** (status) | | `Command_out` | topics **published** to downstream nodes | | lifecycle | a **managed (lifecycle) node**'s states | | `update` | a **timer callback** (or `ros2_control`'s `ControllerInterface::update`) — not `spin`, which is the executor loop | The executive-as-action-server falls right out: a goal arrives (`Command_in`), the feedback channel is "what it's doing" (`State.status`), and it commands subsystems (`Command_out`). Because the shape *is* a ROS node, the bridge is a translation table, not a rewrite — and because the motor and swerve PODs are kept structurally isomorphic to their ROS message targets ([ch. 26](26-portable-motor-interface.md), [ch. 27](27-portable-swerve-interface.md)), the table is mechanical: a swerve drive is a `Twist`-in / `Odometry`-out node with four `JointState` pairs underneath — the shape a `ros2_control` swerve controller takes (cf. `diff_drive_controller`; upstream ships no swerve controller, and the ones that exist are third-party packages). ## What does not map The table is a correspondence, not an identity, and three mismatches keep it honest. ROS topics are asynchronous, many-to-many, and QoS-mediated — a publisher neither knows nor waits for its subscribers — while component wiring is synchronous 1:1 calls in a fixed order. A ROS action goal has an accept/reject/cancel handshake and runs for seconds, while `Command_in` is re-sent every 20 ms with no handshake at all — so the executive-as-action-server analogy holds for the data (goal / feedback / result), not the protocol. And executors, callback groups, and QoS profiles have no analog here, because the component model deliberately has no transport to configure. The mapping earns its keep at the one edge where a real message exists; it is not a claim that a robot of components *is* a ROS graph. ## Keep the semantics, drop the transport There is a trap here, and the model is explicit about avoiding it. The actor/ROS framing tempts you to build a *message bus* — a broker, in-process pub/sub, DDS mimicry. For one roboRIO and one coprocessor, that is ceremony with no payoff, and the outside-robotics survey warns against it directly. So the rule is to **keep the message semantics and drop the message transport:** - **Keep the semantics** — typed, serializable, loggable PODs; a pure `update`; explicit `Config`/`Command`/`State`/`Command_out`. Everything that makes the actor model good. - **Drop the transport** — no broker, no event bus. Wiring is direct typed calls and explicit composition: the outer layer calls `child.update(parent.commandOut)` in dependency order. The decoupling comes from the typed PODs and the pure step, not from a transport. A real message — an actual serialized payload over a wire — appears at exactly **one** edge: the inter-process link between the RIO and a vision coprocessor. Everything on the RIO stays in-process function calls over PODs. ```d2 direction: right RIO: "roboRIO — one process" { EXEC: Executive SUB: Subsystem MOT: Motor EXEC -> SUB: "update() call" SUB -> MOT: "update() call" } COP: "Coprocessor — separate process (PhotonVision / Orange Pi)" COP -> RIO: "the ONE real message (vision pose over the wire)" RIO.style.fill: "#1f3a5a" RIO.style.font-color: "#ffffff" ``` The components inside the RIO are wired by direct calls — same semantics as messages, none of the transport cost. Only the genuinely inter-process edge becomes a real message, and *that* edge is where the ROS bridge earns its keep: the coprocessor can be a ROS node publishing a pose topic, and the RIO's `RobotState` consumes it through the same `Command_in` channel it would use for any observation. ## proto3 is the source of truth What makes the whole thing language-neutral rather than Java-bound is that the channels are defined once in **proto3**, not in a Java interface. From that single schema, `protoc` generates first-class message types for the robot (Java) and the tools (Python, Rust, C++, TypeScript); the thin port shim — the `apply`/`read` interface of [ch. 26](26-portable-motor-interface.md) plus its POD structs, e.g. a generated C++ or Python struct-and-adapter pair for a coprocessor target — is generated for the long-tail targets; and the ROS bridge is generated from the two schemas by applying the two conventions the motor chapter fixed (`None` ↔ `NaN`, `oneof` ↔ mode-enum). Add a control mode and it is one `oneof` arm plus one enum constant, propagating to every binding, the capability set, and the ROS mapping at once. One boundary rule keeps the schema from becoming a 20 ms-loop garbage tax: the generated protobuf types appear **only at the log-and-wire boundary**. In-loop, the channels are plain mutable records/structs generated from the same schema — protobuf-java's immutable, builder-allocating messages would churn the roboRIO's two-core garbage collector every tick, which is why WPILib itself chose QuickBuffers over protobuf-java for its own serialization ([ch. 26](26-portable-motor-interface.md)). This is what "portable" in *the League Architecture* finally means. The Elite Architecture is portable across *seasons* — the seam survives a rewrite. The component model is portable across *languages and frameworks* too: the same component contract describes the robot in Java, a simulation in Python, a controller in a ROS graph, and a tool in TypeScript, because the contract lives in a schema and maps without loss onto the component model the rest of robotics already uses. The shape was never an FRC idiosyncrasy; naming it deliberately is what lets a student's robot code speak the same language as the field it is preparing them for. One honest caveat closes the part: the model is a proposal with real unresolved seams of its own. The [final chapter](32-open-questions.md) lays them out. # 32. Open questions and the road to a build recipe https://frc-code-scout.jointheleague.org/part-3/32-open-questions/ The League model is a living proposal, not finished doctrine, and the honest close to Part III is to name what is settled and what is not. An independent review of the component model found it *conceptually* ready — the four-channel shape, the pure-function discipline, the fill-pattern taxonomy, and the ROS lineage all hold, and the motor and swerve specs are genuinely instances of it — while flagging questions the contract could not leave ambiguous. Two of those have since been **decided**; this chapter records the decisions, then names what genuinely remains open before the model can be wired up and run by default. ## Two questions, decided **`Observations` is the children's `State` — not a fifth channel, and not `Command_in`.** The step signature is `update(Command_in, Observations)`, and the early drafts left the second argument undefined — the estimator row of the taxonomy exposed the tension, taking observations *in* while claiming an empty command channel. Decided, as [ch. 25](25-portable-component-model.md) now states: a component's `Observations` are its **children's (and designated peers') most recent `State`, collected by the outer wiring layer** and passed as `update`'s second argument, with the tick timestamp riding along. They are not `Command_in` — observations are feedback from below, not intent from above — so the four-channel taxonomy survives without a fifth column, and the estimator stops being a special case: `RobotState`'s `Command_in` is genuinely empty, and everything it consumes is observation. **Execution order is state-up, then commands-down.** Each tick runs two passes: first a bottom-up **state pass** — leaves read hardware, and every component's fresh `State` propagates upward — then a top-down **command pass**, executive to motors, computed against this tick's states. This is Part I's read → log → decide → actuate loop expressed over the tree: observe first, then act. (Running the command pass first would decide on *last* tick's states — exactly the one-tick lag the order exists to avoid.) ## Load-bearing open questions **One generic scheduler, or hand-wired composition?** `Command_out` is an array; which child gets which command? A generic outer loop that topologically sorts components and routes `Command_out → Command_in` is possible and very ROS-like, but for FRC scale the recommended path is **explicit hand-wired composition in `RobotContainer`** — clearer, debuggable, and consistent with "drop the transport" ([ch. 31](31-ros-bridge-portability.md)). The more useful artifact than a generic scheduler is a *wiring validator*: a helper that checks every component's `Command_out` has a consumer and every `Command_in` has a producer. **Threading.** The signal-synced odometry thread ([ch. 27](27-portable-swerve-interface.md)) samples at 250 Hz and mutates an inputs struct that a pure 50 Hz `update` is supposed to snapshot. Who owns the synchronization — the L1 adapter, the wiring layer, a lock-free queue? And what does replay record: the full 250 Hz sample queue each tick, or only the latest value? The answer decides whether high-rate odometry lives inside or outside the deterministic boundary, and the model has not given it. **The command adapter.** The model says the executive receives one `Command_in` per tick, but WPILib delivers intent as `Trigger` bindings firing `Command` objects, and PathPlanner delivers autonomous routines as `Command` compositions. Something must adapt those into the executive's `Command_in` — and must define how an operator override preempts a goal mid-sequence, when a new `Command_in` arrives while the superstructure is halfway through a legal transition. That adapter is unbuilt and undesigned. ## Two structural gaps **`RobotState` sits outside the command tree.** "The robot is a tree of components" is strictly true only of intent, which descends a hierarchy. `RobotState` presents the faceplate but not the tree wiring: drive and vision feed it, half the robot reads it, and no command edge touches it ([ch. 28](28-robotstate-superstructure-blocks.md)). The resolution is to let the two graphs be different shapes — intent descends a tree; estimates fan out through a hub, forming a DAG — and make the wiring layer honest about which edges belong to which graph. ```d2 direction: right CMD: "Commands — a tree (top-down)" { E: Executive S1: Subsystem S2: Subsystem M: Motor E -> S1 E -> S2 S1 -> M } ST: "State — a DAG (many consumers)" { DRIVE: Drive VIS: Vision RS: RobotState EXEC: Executive AIM: Aim DRIVE -> RS VIS -> RS RS -> EXEC RS -> AIM } ``` **Config versus mode switches.** The boundary test — "changes every loop → `Command`; identifies across a session → `Config`" — covers the extremes but not the gray middle: things that change *between matches but not during* (an interlock table, a mechanism toggled on or off). The single `reconfigure(partialConfig)` door conflates two concerns. Split it: **`tune(partialConfig)`** for runtime parameter adjustment (gains, limits) that needs no lifecycle change, and **`reconfigure(partialConfig)`** for structural changes (interlock tables, enabling a mechanism) that may require a `disable → reconfigure → enable` lifecycle transition. ## The road to a build recipe Two more questions sit beneath these, both about how the model meets WPILib in practice. A team adopting it either *replaces* WPILib's `CommandScheduler` with a custom executor (high ceremony, fighting the framework) or *wraps* each component in a WPILib `Subsystem` whose `periodic()` delegates to `update()`, with the wiring in `RobotContainer.periodic()`. The pragmatic endorsement is the **wrap**: it keeps the pure-function contract intact while working *with* WPILib, and it is the path the elevator example of [ch. 25](25-portable-component-model.md) already sketches. Separately, the swerve spec surfaces the single most concrete unbuilt artifact this whole research produced — a **`TunerConstants → ModuleConstants` adapter** that hands CTRE teams the generator's numbers without the generator's types. What must close before the model becomes a recipe — before `scaffold-robot` and `add-subsystem` (the build tooling referenced throughout) emit every component to this contract by default — is the open list above: the hand-wired-plus-validator choice, the threading answer, the command adapter, the dual-graph wiring, the `tune`/`reconfigure` split, and the WPILib-wrap example — plus one discipline already decided but not yet demonstrated under load: the allocation rule (plain mutable structs in-loop, protobuf only at the log-and-wire boundary, [ch. 26](26-portable-motor-interface.md)) held to through a real 20 ms loop. None of them is conceptual; all of them are the difference between a design that is right on paper and one a team can run on a real robot in build season. That is the honest end of the book. Part I showed the architecture top teams converged on; Part II opened the hood on each piece; Part III argued that the pieces are one shape and named what naming the shape buys — telemetry, replay, tests, lifecycle, and language portability at every scale — while being candid that the recipe is not yet finished. The shape is right. The wiring is the work that remains. # Part IV — Scoring Elite Code https://frc-code-scout.jointheleague.org/scoring/ # Part IV — Scoring Elite Code *The instrument, its calibration against real results, and one team's four-year climb.* Parts I–III argue *what* elite robot code looks like and *how* to build it. Part IV is how you **measure** it — and the evidence that the measurement means something. Three chapters, three questions: *what do you measure?*, *does it actually track winning?*, and *what does the climb look like on one real team?* ## Chapters 33. [The rubric in full](33-the-rubric.md) — the eight-dimension instrument, every 0–4 anchor and the grep/AST cheat-sheet, scored directly. 34. [The San Diego scoresheet](34-the-san-diego-scoresheet.md) — the rubric run on 24 teams against season-matched Statbotics EPA: does better code track winning? (Yes — moderately, and unevenly.) 35. [The Patribots, four years](35-the-patribots-four-years.md) — one team scored season by season with full commit history: the rubric in motion, and the two rules it illustrates. Two validation datasets appear across these chapters and are easy to conflate: ch. 33 cites a **cross-validated study** over the full indexed corpus (55 teams; the cross-validation pooled every available season repo per team — 232 team-years — while rubric scoring uses each team's most recent season), and ch. 34 is a **hand-scored companion study** of 24 San Diego teams paired with season-matched EPA. They are different samples answering related questions. The method *behind* the rubric — how the corpus was read and why the dimensions are what they are — lives in [Appendix A — How We Developed This](../appendices/how-we-developed-this/). # 33. The rubric in full https://frc-code-scout.jointheleague.org/scoring/33-the-rubric/ **This is the complete, authoritative rubric — eight dimensions, each scored 0–4 against anchored, observable indicators, with half-steps allowed.** The companion chapter [how to read the rubric](../appendices/how-we-developed-this/02-the-rubric.md) explains why it is shaped this way and how to use it; this page carries every anchor, every caution, and the grep/AST cheat-sheet in full, so it can be scored from directly. Three terms carry weight throughout and are defined earlier in the book: the **IO line** and **loop-above vs loop-below** ([Part II ch. 16](../part-2/16-hardware-abstraction.md)), and **goal-fanout** — a coordinator turning one robot-wide goal into per-subsystem states ([Part I ch. 5](../part-1/05-the-coordination-seam.md)). On effort: budget roughly an hour per repo for a careful confirmed score — the grep pass takes minutes; opening the files behind every positive hit is the bulk of the work. --- ## Why dimensions, not a single ladder score The [maturity ladder](../appendices/how-we-developed-this/03-the-maturity-ladder.md) is right about *sequence within a dimension* — nobody reaches unit tests without an IO layer, because the IO layer is what makes subsystems testable. But real teams adopt unevenly *across* dimensions: a team can have a clean IO layer and zero tests, or AdvantageKit logging bolted onto spaghetti coordination, or Choreo trajectories driven by dead-reckoned odometry. A single ladder score hides exactly the gaps that matter most — "you've built the IO layer, tests are one step away, and you're not taking it." So: score each dimension independently, report the vector, and read the *shape* of the profile, not the sum. One revision to the ladder this implies — the teaching order bundles "IO layer + simulation + lightweight logging" as one leap and "FSM + tests + replay" as another; in the wild these decompose. That bundling is the right *teaching* order but the wrong *measurement* instrument, so the rubric splits them. --- ## Scoring rules - **Unit** — each team's most recent real competition-season repo (2025 or 2026; not templates, training repos, or off-season toys). Record the repo name and season on the scoresheet. - **Adjacent repos count for one dimension only** — separate team-library repos (`SuperCORE`, `WarlordsLib`, `3128-common`, `NOMADBase`) and training repos count toward **D8 (Sustainability)** but not toward the code dimensions, which are scored from the season repo alone. - **Score what's used, not what's present.** A Choreo vendordep JSON with no `.traj` files and no Choreo imports is not Choreo adoption. An empty `src/test` folder is not testing. Confirm every indicator by opening at least one file — see [Part I ch. 3 — the IO seam](../part-1/03-the-io-seam.md) for why the seam is the load-bearing structure to confirm. - **Half-steps are allowed** (e.g., 2.5) when a team sits clearly between anchors. Don't agonize. A legitimate 2.5 on D1 is IO interfaces on most mechanisms but selection still scattered across files — confirmed past the L2 anchor, short of L3. An illegitimate 2.5 is splitting the difference because you couldn't tell whether the `Superstructure` is real — that's an unconfirmed candidate, not a half-step; go open the file. - **Known blind spot** — repos are shallow clones with `.git` stripped, so commit history, PRs, and contributor counts are unobservable. D8 is scored from artifacts only and should be treated as a floor, not a ceiling. - **Language note** — anchors are written in Java/WPILib terms. Kotlin (6695) and Python/RobotPy teams hit the same anchors with different syntax: MagicBot's framework FSM counts at D2 level 2, its component injection counts at D1 level 2. --- ## Corpus prevalence (measured) From the tree-sitter → DuckDB index of 55 season repos. Use these to calibrate: a marker present in 3 teams is a ceiling signal; one present in 45 is table stakes. Confirm by reading regardless. | Marker | Teams (of 55) | Calibration note | |---|---|---| | `commands/` dir · `Constants` · `subsystems/` dir | 54 · 54 · 53 | universal — no signal | | `addVisionMeasurement` call | 50 | vision/pose-est is assumed (D7 ≥ 2 is the floor, not the ceiling) | | PhotonVision import | 42 | | | `*PoseEstimator` · `*RobotState` class | 36 · 26 | pose-est ≫ centralized world-model (D7 L2 vs L4 split is real) | | `util/` dir · `lib/` dir | 36 · 26 | lib/robot split (D8) in ~half | | `*IO` interface · `*Inputs` struct | 24 · 26 | **IO seam ≈ 44%; every IO team has an Inputs struct (0 exceptions)** | | `*IOSim` impl · `@AutoLog` | 19 · 19 | | | `generated/` dir (CTRE Tuner swerve) | 19 | a real structural element worth recognizing | | `Superstructure` class | 22 | **the dominant coordinator name** | | device-named HW impl (`*IOTalonFX` 14 / `*IOPigeon2` 12 / `*IOLimelight` 12 / `*IOSparkMax` 7) | — | **this is how hardware impls are named** | | generic `*StateMachine` | 12 | second coordination marker | | `*IOReal` | **5** | ⚠ rare — do **not** grep for this to find an IO layer | | `jgrapht` · `RobotManager` · `WantedState` enum · `*IONull` · replay IO variant · BehaviorTree | 3 · 2 · 2 · ~1 · 1 · 1 | true ceiling markers; a hit is a strong D2/D5 signal | | vendor type (`com.ctre`/`com.revrobotics`) imported *above* the IO line | 22 of 24 IO teams | clean vendor confinement is aspirational, not the norm — **judge it by reading, not by filename**: some teams confine vendor types in wrapper classes *not* named `*IO` (e.g. 4738's `Kraken.java`/`SafeSpark.java`), which a `*IO`-name heuristic wrongly flags as a leak | Most-common subsystems (by `subsystems/`): **vision · intake · drive · shooter · climber · elevator**, then arm · LEDs · indexer · turret. Manipulator is rare; subsystem names are game-dependent — score the IO/coordination *structure*, not the mechanism roster. --- ## What predicts competition results — and why you must read the code Measured 2026 against Statbotics EPA, leave-one-**team**-out cross-validated over 232 team-years / 55 teams, cluster-bootstrap CIs. (The cross-validation pooled every available season repo per team — 232 team-years across the 55 teams — while rubric scoring uses each team's most recent season.) **The two validation datasets.** This section reports the cross-validated study over the full indexed corpus. The book's other validation — [the San Diego scoresheet](34-the-san-diego-scoresheet.md) — is a companion study: 24 local teams hand-scored on this rubric and paired with season-matched EPA. The two are different samples answering related questions; where this section calibrates the *instrument* (confirmed vs mechanical passes, per-dimension trust), ch. 34 asks whether the scores track winning in one region. Three results should shape how you use this rubric: 1. **Confirming use, not presence, roughly doubles the rubric's predictive validity.** On the same 55 teams, the **agent-confirmed** D1–D8 (a model that opened the files) predicts EPA at Spearman **ρ ≈ 0.53**; the **mechanical candidate** pass (grep/SQL hits, scored as presence) reaches only **ρ ≈ 0.29**. The paired difference is significant (95% CI `[0.04, 0.44]`). This is the empirical case for the golden rule — the cheap pass is a lead sheet, not a score. 2. **Most of what predicts EPA from "code" is just size and program age — not engineering quality.** A model of ~50 raw code features hits ρ ≈ 0.58, but **codebase size + program maturity alone scores ≈ 0.60**, while the *sophistication* features (the rubric's structural patterns, size removed) score ≈ 0.38, and a within-team (fixed-effects) view collapses to ≈ 0.05. Bigger, older, better-resourced programs have both more code and better results. Do not read a high raw correlation as "good code wins" — and do not reward sheer volume. 3. **The mechanical pass is trustworthy on some dimensions and not others.** Agreement (quadratic-κ) between the mechanical candidate and the agent-confirmed level: | Dimension | κ (mechanical vs confirmed) | Trust the grep? | |---|---|---| | D4 Testing | 0.86 | yes — `src/test` + asserts is unambiguous | | D1 Architecture | 0.82 | yes — `*IO` interfaces are concrete | | D3 Simulation | 0.81 | yes | | D2 Coordination · D5 Logging | 0.74 | mostly — confirm the coordinator is real, logging covers all subsystems | | **D6 Auto/Path · D7 Vision** | **0.60** | **no — confirm by reading** (files present ≠ trajectories driven / vision fused) | | **D8 Sustainability** | **0.57** | **no — read the README/CI/library; history is shallow** | Spend your reading budget where κ is low: **D6, D7, D8** are where "present" most diverges from "used." One design decision deserves its own sentence: **the eight dimensions are equally weighted, deliberately.** An EPA-optimal re-weighting was tested and was not distinguishable from equal weight on this sample — so don't over-engineer the sum. --- ## D1 — Hardware decoupling (architecture) *The ladder's spine — how far has the team pushed the boundary between subsystem logic and physical devices?* | Level | Anchor | Observable indicators | |---|---|---| | 0 | Everything in `Robot.java` / timed-robot blob | No subsystem classes, or one giant file; motor objects and game logic interleaved | | 1 | Command-based baseline | `SubsystemBase` subclasses own motors directly; commands call subsystem methods; a `Constants` file exists | | 2 | Partial or vendored abstraction | Vendored swerve abstraction (YAGSL, CTRE swerve generator) but mechanisms still hardware-welded; or IO interfaces on one or two subsystems only | | 3 | IO layer as the default | Per-subsystem `*IO` interfaces with at least Real + Sim implementations across most mechanisms; selection in one place (switch/factory) | | 4 | Generalized / library-grade | Generic parameterized bases (254-style `ServoMotorSubsystem`), null-object IO variants, or replay IO variants; the abstraction is reused, not repeated | **Grep:** `interface .*IO` (the spine), plus its implementations — `*IOSim*` (the sim impl, 19 teams) and a **hardware impl named by device**: `*IOTalonFX*` / `*IOKraken*` / `*IOSparkMax*` / `*IOSpark*` / `*IOPigeon2*` / `*IONavX*` / `*IOLimelight*` / `*IOPhoton*`. **Do not rely on `*IOReal*`** — only ~5 teams use that name; the robust signal is *an `interface *IO` with ≥2 implementations, one of them `*IOSim`*. Also `@AutoLog`, YAGSL config dirs (`src/main/deploy/swerve`), `TunerConstants` (CTRE generator). At L4: a generic base (`MotorIO`/`ServoMotorSubsystem`) reused across mechanisms — the `*IONull*` null-object variant is real but near-absent in the corpus (~1 team), so don't require it. **Distinguish at level 3:** an IO *directory* of concrete hardware wrappers (the 2056 case) is not an IO *layer* — there must be an interface with swappable implementations. Check for an actual `interface` and at least two implementations. --- ## D2 — Coordination & decision logic *How does the robot decide what to do and keep mechanisms from fighting?* | Level | Anchor | Observable indicators | |---|---|---| | 0 | Imperative teleop | Joystick values mapped straight to motor outputs in a periodic method; no command composition | | 1 | Command composition | Sequential/parallel command groups; button bindings in `RobotContainer`; autos as command sequences | | 2 | Explicit state machines | Wanted/current enums per subsystem (2910 style) or a centralized `RobotManager` FSM (581 style); a transition function owns state changes | | 3 | Superstructure coordination | A coordinator object fans robot-wide goals out to subsystems; intent (requested state) is separated from execution; kinematic safety handled deliberately (motion planner or guarded transitions) | | 4 | Search/graph-based or beyond | State graph with pathfinding (JGraphT / A* over states), behavior tree runtime, or equivalent — transition logic as data, not code | **Grep:** `Superstructure` (the dominant coordinator — 22 teams; check it's a real goal-fanout, not a holder), generic `*StateMachine` (12 teams). Rarer variants, each a strong signal when present but few teams: `enum WantedState`/`SystemState` (2910 style — 2 teams), `RobotManager` (581/3128 centralized FSM — 2 teams), `handleStateTransitions`, `jgrapht` (3), `AStarSolver`, `BehaviorTree` (1). Don't weight `WantedState`/`RobotManager` as the default — they're niche; the common path is a `Superstructure` (level 3) optionally backed by a `*StateMachine`. A newer L3 marker worth recognizing: a **request-based API** — a `Goal`/`SuperstructureRequest` enum or sealed type plus a `requestGoal(...)`/`request(...)` method (often returning a `Command`) and a `handleStateTransitions`-style guard — which makes intent-vs-execution explicit. **Caution:** a class *named* Superstructure that just holds references is level 1 wearing a level 3 name. Look for an actual goal-request API and transition logic. --- ## D3 — Simulation *Can the code run, and surprise you, without the robot?* | Level | Anchor | Observable indicators | |---|---|---| | 0 | None | No `simulationPeriodic`, no sim classes | | 1 | Token sim | `simulationPeriodic` stubs or a sim GUI run that echoes setpoints; no physics | | 2 | Mechanism physics sim | WPILib physics classes (`ElevatorSim`, `SingleJointedArmSim`, `FlywheelSim`, `DCMotorSim`) wired into IO sim implementations for the main mechanisms | | 3 | Whole-robot sim workflow | Sim covers drivetrain + mechanisms + (ideally) vision; maple-sim or equivalent dynamics; evidence the team develops in sim (sim-specific configs, sim auto-testing mode) | | 4 | Sim/replay as primary verification | Deterministic re-simulation or log replay of real matches treated as a workflow (replay IO variants, ideal-sim variants, 4481 style) | **Grep:** `simulationPeriodic`, the WPILib physics classes `ElevatorSim|SingleJointedArmSim|FlywheelSim|DCMotorSim`, maple-sim's `SwerveDriveSimulation` (WPILib ships no swerve-drive sim class), `maple-sim` / `org.ironmaple`, `*IOReplay*`, `IdealSim`. --- ## D4 — Testing & verification *The IO layer's deferred dividend. Almost no team collects it — the sharpest discriminator in the corpus.* | Level | Anchor | Observable indicators | |---|---|---| | 0 | None | No `src/test`, or only the GradleRIO boilerplate test | | 1 | Token tests | A handful of trivial tests (constants, math utilities); not run anywhere | | 2 | Real unit tests | Tests that construct sim-backed subsystems and assert on behavior; meaningful coverage of at least a few mechanisms | | 3 | Tests in CI | `.github/workflows` runs `gradle test` (not just build) on push/PR; tests gate merges | | 4 | Command-level verification | Tests run actual commands to completion in simulation and assert on resulting state (SciBorgs `runToCompletion` style); broad suite (10+ test files) | **Grep:** `src/test/java` tree, `@Test`, `assertEquals`, `.github/workflows/*.yml` containing `test`, `runToCompletion`, `CommandTestBase`. **Note the asymmetry with D3:** physics sim without tests (common) scores D3 high / D4 low. That gap is the single most actionable finding for a team — flag it in notes. --- ## D5 — Logging, telemetry & diagnostics *"When the robot misbehaves on the field, how do we know why?"* | Level | Anchor | Observable indicators | |---|---|---| | 0 | Prints | `System.out.println` debugging or nothing | | 1 | Dashboard values | `SmartDashboard.put*` / Shuffleboard scattered through subsystems | | 2 | Structured lightweight logging | DogLog, Epilogue (`@Logged`), URCL, or systematic NetworkTables publishing; AdvantageScope layouts committed | | 3 | AdvantageKit | `@AutoLog` inputs structs, `Logger.processInputs` throughout; full-match logging to file | | 4 | Replay + operational diagnostics | Log replay actually exercised (replay IO variants, replay vendordep configs) and/or self-check fault reporting (3061/3015-style `FaultReporter`, system-check commands) | **Grep:** `doglog`, `Epilogue`, `@Logged`, `URCL`, `org.littletonrobotics.junction`, `Logger.processInputs`, `@AutoLog`, `FaultReporter`, `SystemCheck`, committed `.json` AdvantageScope layouts. **Caution:** AdvantageKit vendored but with `processInputs` on one subsystem out of ten is level 2, not 3. The level is about coverage, not presence — gauge coverage by the *count* of `Logger.processInputs` / `recordOutput` call sites relative to the subsystem roster (a handful = partial; dozens across every subsystem = real L3). --- ## D6 — Autonomous & path planning *Are the three path concerns (authored path, optimal trajectory, reactive avoidance) present and pulled apart?* | Level | Anchor | Observable indicators | |---|---|---| | 0 | Timed / dead-reckoned auto | Drive-by-stopwatch autos; no trajectory following | | 1 | Basic closed-loop auto | Encoder/gyro-based moves; maybe WPILib trajectory following on a simple path | | 2 | PathPlanner autos | `.path`/`.auto` files, PathPlannerLib configured with real constraints; multiple competition autos | | 3 | Optimized trajectories | Choreo (`.traj`/`.chor` files actually referenced in code) where time matters, typically alongside PathPlanner; on-the-fly driving to poses (align-to-target commands) | | 4 | Reactive planning | Repulsor/potential-field or equivalent dynamic obstacle avoidance; superstructure states exposed as auto actions; auto selection infrastructure | **Grep:** `pathplanner` dir under `src/main/deploy`, `PathPlannerLib.json`, `choreo` / `.traj` / `.chor`, `Repulsor`, `RepulsorField`, `AutoBuilder`, `AutoBuilder.pathfind*` (on-the-fly, L3). **Confirm use — this is the lowest-trust dimension (κ 0.60).** Choreo `.traj`/`.chor` *files* count for nothing on their own: 15 teams in the corpus carry Choreo files they **never reference in code**, and PathPlanner `.auto` files frequently carry `choreoAuto:false`. Require an actual code reference — `fromChoreoTrajectory(...)`, `Choreo.loadTrajectory(...)`, a `choreo.auto.AutoFactory` — before crediting Choreo at L3. Likewise confirm `AutoBuilder.configure(...)` is wired with real constraints, not just imported. --- ## D7 — Localization & vision *What does the robot believe about where it is, and how is that belief maintained?* | Level | Anchor | Observable indicators | |---|---|---| | 0 | None | No vision, odometry unused in decisions | | 1 | Targeting only | Limelight tx/ty servoing on a target; no pose estimation | | 2 | Pose estimation | AprilTag pose via PhotonVision/Limelight MegaTag feeding `SwerveDrivePoseEstimator.addVisionMeasurement` | | 3 | Fused, filtered estimation | Vision std-dev tuning, multi-camera fusion, rejection logic; vision behind an IO interface with sim variant | | 4 | World model as architecture | A dedicated `RobotState` class owning pose + game-piece state with time-interpolated buffers; localization decoupled from control | **Grep:** `photonlib`, `PhotonCamera`, `LimelightHelpers`, `addVisionMeasurement`, `SwerveDrivePoseEstimator`, `RobotState`, `TimeInterpolatableBuffer`, `MegaTag`. For the **L3 fused-and-filtered** rung, the discriminating markers are `setVisionMeasurementStdDevs` (dynamic std-dev tuning) and a `VisionIO` interface with a sim variant (vision decoupled from `Drive`); for **L4**, a dedicated `RobotState` whose pose lives in a `TimeInterpolatableBuffer`/`ConcurrentTimeInterpolatableBuffer`. **Confirm use (κ 0.60).** `addVisionMeasurement` is near-universal (50/55 teams) — it is the L2 floor, not evidence of L3. The level is set by the *filtering* (rejection logic, per-tag std-devs) and *architecture* (IO seam, central world-model), which you must read to see. --- ## D8 — Sustainability & process (artifact-based) *Will this codebase survive its seniors graduating? Scored from artifacts only — treat as a floor.* | Level | Anchor | Observable indicators | |---|---|---| | 0 | Bare code drop | No README beyond GradleRIO default; no docs, no structure conventions | | 1 | Basic hygiene | Real README (build/deploy instructions); consistent package structure; constants organized | | 2 | Onboarding & standards | Contributing/style docs; formatter or linter configured (spotless/checkstyle in `build.gradle`); training or template repos exist in the team's org | | 3 | CI + team library | GitHub Actions on the season repo; a maintained season-independent library or `lib/` package carried across seasons (`SuperCORE`, `WarlordsLib`, `3128-common`, `NOMADBase` pattern) | | 4 | Program-grade infrastructure | Library is versioned/consumed as a dependency (not copy-paste); multi-robot variant structure; docs generated or maintained; evidence of release discipline | **Grep:** `README.md` length and content, `CONTRIBUTING`, `spotless`, `checkstyle`, `.github/workflows`, separate `lib`/`common` repos in team folder, `shared/` or variant packages. --- ## Scoresheet template One row per team. The sum is reported, but the profile is the finding. | Team | Repo scored | D1 Arch | D2 Coord | D3 Sim | D4 Test | D5 Log | D6 Auto | D7 Vision | D8 Sustain | Σ /32 | Profile notes | |---|---|---|---|---|---|---|---|---|---|---|---| | | | | | | | | | | | | | --- ## Reading the profile: common shapes - **Balanced climber** — all dimensions within ±1 of each other. The ladder is working; next step is whatever's lowest. - **Architecture without verification** (D1 ≥ 3, D3/D4 ≤ 1) — adopted the IO layer's *form* without its *payoff*. Likely copied a template (check against 5712-style AdvantageKit templates). Highest-leverage intervention: one unit test. - **Tooling adopter** (D5/D6 high, D1/D2 low) — uses AdvantageScope/PathPlanner/Choreo but the code underneath is baseline command-based. Tools were installable; architecture wasn't. Intervention: an IO layer on one subsystem. - **Template inheritor** — D1 = 3 exactly matching a known public template, everything else low. Distinguish from organic adoption by checking whether IO interfaces exist for *their* mechanisms or only the swerve they forked. - **Legacy program** — competent older patterns (solid command-based, good autos) with no post-2022 tooling. Different conversation: modernization, not fundamentals. - **Verification ceiling** (everything ≥ 3 except D4) — the regional-elite profile; even strong teams rarely test. D4 ≥ 2 is the rarest marker in the national corpus and the clearest signal of real software-engineering culture. --- ## Suggested scoring procedure 1. Identify the latest real season repo; record season and language. 2. Run the grep cheat-sheet (one pass per dimension) to generate candidate levels. 3. Open the files behind every positive hit — confirm use, not presence. Adjust the level. 4. Check the team's other repos *only* for D8 (libraries, templates, training). 5. Write 2–3 sentences of profile notes: the shape, the likely explanation, the one highest-leverage next step for the team. A worked example of what step 2 produces — the raw mechanical-candidate output for one Reefscape 2025 repo, with per-dimension AST hits and the files to open — lives in the repository at `knowledge/examples/sample-score-output-reefscape2025.md`. It is a lead sheet in exactly the sense above: a heuristic Σ floor plus a list of files to confirm, not a score. # 34. The San Diego scoresheet https://frc-code-scout.jointheleague.org/scoring/34-the-san-diego-scoresheet/ **This whole book makes a claim — that the architecture on these pages is what separates elite robot code from the rest — and a claim that big deserves to be checked against reality, not just asserted.** So we ran the rubric on our own backyard. We scored 24 active San Diego FRC teams on the D1–D8 dimensions, paired each with its season-matched [Statbotics](https://www.statbotics.io/) EPA — the modern, normalized strength rating built purely from match results — and asked the blunt question: does better code actually correlate with winning? The short answer is *yes, moderately, and unevenly* — and the uneven part is where the real lessons live. This chapter is the receipt. For what the architecture is *supposed* to predict, see [what the architecture predicts](../appendices/how-we-developed-this/04-what-it-predicts.md); for the scoring instrument itself, see [the rubric in full](33-the-rubric.md). ## What the study was Twenty-four active San Diego FRC teams, scored on the eight-dimension code-sophistication rubric, each paired with the Statbotics EPA of the exact season repo we scored. This is a hand-scored regional study, distinct from (and a companion to) the 55-team, 232-team-year cross-validated study reported in [the rubric in full](33-the-rubric.md#what-predicts-competition-results--and-why-you-must-read-the-code): that study calibrates the instrument on the full indexed corpus; this one asks whether the scores track winning among our neighbors. Five more teams were excluded as inactive or legacy — their newest competition code predates the 2024 WPILib baseline the rubric measures, or no real season repo exists — and since those teams also carry no recent EPA, dropping them doesn't bias the correlation. Two methodology calls shaped the numbers: D1 uses the lenient reading (a maintained 254-style generic base counts toward the top of D1 even without a Real/Sim IO swap), and D8 credits a team library that demonstrably exists. The dimensions, abbreviated throughout: **D1** Architecture · **D2** Coordination · **D3** Simulation · **D4** Testing · **D5** Logging · **D6** Autonomous/Path planning · **D7** Vision/Localization · **D8** Sustainability. EPA is season-matched to the scored repo; the state percentile (`st%ile`) is that team's standing within California *for that season*, so a 2023 repo is ranked against 2023 California, not 2026. The raw data behind every table below lives in two machine-readable files. `sd-frc-master.csv` carries one row per team — team number, name, season, language, the eight rubric scores, the total, and four external metrics (`norm_EPA`, `state_pctile`, `winrate`, `epa_points`). `sd-frc-correlations.csv` carries one row per dimension — its code (D1…D8) and its Spearman ρ against each of the three external measures (`spearman_normEPA`, `spearman_state_pctile`, `spearman_winrate`). The tables here are those two files, read straight. ## The full scoresheet Twenty-four active teams, ranked by code total. Σ is out of 32. | # | Team | Season | Lang | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | Σ/32 | normEPA | st%ile | win% | |--|--|--|--|--|--|--|--|--|--|--|--|--|--|--|--| | 1 | 3647 Millennium Falcons | 2025 | Java | 3.5 | 3 | 3 | 0 | 2 | 3 | 3 | 2.5 | 20 | 1456 | 28 | 44 | | 2 | 4738 Patribots | 2025 | Java | 3 | 2.5† | 2 | 0 | 3 | 3 | 3 | 3 | 19.5† | 1735 | 96 | 77 | | 3 | 5137 Iron Kodiaks | 2026 | Java | 3.5 | 2 | 2.5 | 0 | 3 | 2 | 2.5 | 2 | 17.5 | 1594 | 78 | 46 | | 4 | 1538 Holy Cows | 2025 | C++ | 3 | 2 | 1.5 | 0.5 | 2 | 3 | 3 | 2 | 17 | 1670 | 92 | 65 | | 5 | 3128 Aluminum Narwhals | 2025 | Java | 3.5 | 3 | 1 | 0 | 1 | 3 | 2.5 | 3 | 17 | 1721 | 94 | 70 | | 6 | 6995 NOMAD | 2026 | Java | 3 | 1.5 | 1.5 | 0 | 1.5 | 3 | 3 | 2 | 15.5 | 1669 | 88 | 62 | | 7 | 3255 SuperNURDs | 2026 | Java | 2 | 2 | 0.5 | 0 | 2 | 3 | 2.5 | 3 | 15 | 1744 | 96 | 62 | | 8 | 2485 Overclocked | 2026 | Java | 2 | 1.5 | 1 | 0 | 2 | 2.5 | 3 | 2 | 14 | 1537 | 67 | 44 | | 9 | 2102 Team Paradox | 2026 | Java | 2 | 1 | 3 | 0 | 1.5 | 3 | 2 | 1 | 13.5 | 1658 | 87 | 64 | | 10 | 3341 Option 16 | 2026 | Java | 2 | 2 | 1.5 | 0 | 1.5 | 2 | 2 | 1.5 | 12.5 | 1447 | 24 | 48 | | 11 | 1572 Hammer Heads | 2024 | Java | 2 | 1.5 | 1 | 0 | 2 | 2 | 2 | 1.5 | 12 | 1523 | 56 | 54 | | 12 | 6695 GalvaKnights | 2026 | Kotlin | 1.5 | 1 | 0.5 | 2 | 1 | 2 | 1.5 | 2.5 | 12 | 1564 | 72 | 41 | | 13 | 2658 Sigma Motion | 2026 | Java | 2 | 1 | 1 | 0 | 2 | 2 | 2 | 1.5 | 11.5 | 1479 | 42 | 52 | | 14 | 4160 RoBucs | 2026 | Java | 2 | 1 | 1.5 | 0 | 1.5 | 2 | 2 | 1.5 | 11.5 | 1485 | 46 | 41 | | 15 | 8891 Wild Raccoons | 2024 | Java | 1.5 | 2 | 0.5 | 0 | 1 | 2.5 | 1.5 | 2 | 11 | 1515 | 52 | 52 | | 16 | 3749 Team Optix | 2023 | Java | 1.5 | 1 | 0.5 | 0 | 1 | 2 | 2.5 | 2 | 10.5 | 1574 | 75 | 47 | | 17 | 4919 Team Ronin | 2026 | Java | 1.5 | 1 | 1 | 0 | 1.5 | 2 | 2 | 1.5 | 10.5 | 1449 | 26 | 46 | | 18 | 0812 Midnight Mechanics | 2026 | Java | 1.5 | 1 | 1.5 | 0 | 1 | 1.5 | 2 | 1.5 | 10 | 1546 | 68 | 56 | | 19 | 9573 MarauderTech | 2026 | Java | 2 | 1 | 1 | 0 | 1 | 1 | 2 | 1 | 9 | 1415 | 12 | 21 | | 20 | 9730 Metal Maniacs | 2026 | Java | 1 | 1 | 1 | 0 | 1 | 2 | 1.5 | 1 | 8.5 | 1470 | 36 | 33 | | 21 | 2839 Daedalus | 2026 | Java | 1.5 | 1 | 0.5 | 0 | 0.5 | 1 | 0 | 1 | 5.5 | 1347 | 2 | 25 | | 22 | 2984 Vikings | 2024 | Python | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 5 | 1462 | 29 | 22 | | 23 | 4419 Team Rewind | 2024 | C++ | 1 | 1 | 0 | 0.5 | 1 | 0.5 | 0 | 1 | 5 | 1564 | 72 | 46 | | 24 | 5025 Pacific Steel | 2023 | Java | 1 | 1 | 0 | 0 | 1 | 0.5 | 0 | 1 | 4.5 | 1492 | 46 | 55 | *† This survey pass scored 4738's 2025 season from the shallow clone. The full-history re-score in [the Patribots case study](35-the-patribots-four-years.md) confirmed the Superstructure's guarded transitions and raised D2 to 3, for Σ = 20.0. Both numbers are kept as scored; the correlations here use the survey values.* ## What predicts results Rubric total versus normalized EPA lands at Spearman **ρ = 0.55** (Pearson r = 0.60, p ≈ 0.002). Better software *is* associated with better results — but it explains only about a third of the variance. The headline number is real, and it is modest; anyone who tells you clean code guarantees banners is overselling. The per-dimension breakdown is the more useful cut. Spearman ρ against each external measure, n = 24, sorted by how strongly the dimension tracks the two most robust measures: | Dimension | vs normEPA | vs state %ile | vs win rate | |---|---|---|---| | D8 Sustainability | 0.60 | 0.62 | 0.45 | | D6 Autonomous/Path | 0.59 | 0.60 | 0.59 | | D7 Vision/Localization | 0.51 | 0.52 | 0.47 | | D2 Coordination | 0.41 | 0.41 | 0.49 | | D1 Architecture | 0.39 | 0.41 | 0.44 | | D5 Logging | 0.35 | 0.36 | 0.39 | | D4 Testing | 0.26 | 0.26 | −0.01 | | D3 Simulation | 0.17 | 0.20 | 0.33 | | **Total Σ** | **0.55** | **0.56** | **0.51** | Three dimensions carry the signal — **Sustainability (D8)**, **Autonomous/Path planning (D6)**, and **Vision/Localization (D7)** — and two barely register: **Simulation (D3)** and **Testing (D4)**. (The numbers are in the table; the prose that follows is the interpretation.) That split has a clean reading. D6 and D7 are the dimensions that put points on the board *directly* — a team that runs real autos and aligns to targets with vision scores more, immediately. D8 (CI, a carried team library, real docs, a formatter) is a proxy for overall program maturity and resourcing, which sustains competitiveness across a season. D3 and D4 — simulation and testing — are internal engineering-quality investments whose payoff is robustness and developer velocity, not raw match points; teams adopt them for reasons largely orthogonal to a given season's standings. And a caution on D4: it is near-constant here — almost every team scores 0 — so its low correlation is partly a low-variance artifact, not evidence that testing hurts. The one team that genuinely tests, 6695 GalvaKnights, sits mid-pack in the *code* ranking (12th of 24) while posting a 72nd-percentile EPA — a result that, if anything, argues for the testing investment rather than against it. ## The outliers are the interesting part A correlation of 0.55 is loose enough that the mismatches carry most of the signal. Three profiles are worth memorizing. **Sophisticated code, weak results — 3647 Millennium Falcons.** The single most feature-complete codebase in San Diego (Σ = 20: maple-sim whole-robot simulation, AdvantageKit, 254-style architecture, multi-camera vision, Choreo, spotless + CI) finished in the *bottom third* of California in 2025 — state percentile 28, a 44% win rate. Their software comes close to a working model of this book's architecture; their 2025 on-field result did not follow. This is the textbook case that code sophistication and competition performance are different axes, and that a robot is more than its repo. **Modest code, strong results — 4419 Team Rewind and 3749 Team Optix.** Rewind's minimal 14-file C++ repo (Σ = 5) paired with a 72nd-percentile 2024 EPA; Optix's 2023 repo (Σ = 10.5) with percentile 75. Whatever drove those results, it wasn't software the rubric can see — a fast, reliable mechanism and a good driver don't show up in an AST scan. **The aligned top — 4738 Patribots and 3128 Aluminum Narwhals.** Patribots is the cleanest case of code and results agreeing: Σ = 19.5 in this survey pass (20.0 on the full-history re-score — see the table footnote), a real AdvantageKit IO layer across *every* mechanism, and the best competition record in the set — state percentile 96, 77% wins. 3128 pairs Σ = 17 with percentile 94. When the architecture is real and the program is strong, the two axes line up — which is the outcome this book is arguing for. The Patribots' four-year climb to that point is its own study; see [the Patribots case study](35-the-patribots-four-years.md). ## A few per-team notes worth keeping - **5137 Iron Kodiaks** — the most balanced sophisticated codebase: a generalized `MotorIO` layer with three hardware implementations, AdvantageKit across six subsystems, physics sims (Shooter/Intake/SwerveModule). Highest D3 among the AdvantageKit teams. - **1538 Holy Cows** — the only C++ program of note: a custom framework (CowLib, a hand-rolled command system, a threaded `Localizer` with PhotonVision pose fusion, Choreo). Sophistication reached without WPILib's Java idioms — and it shows in the results (state percentile 92). - **6995 NOMAD** — a Real/Sim/None vision-and-climb IO layer *without* AdvantageKit logging: architecture-forward, telemetry-light. - **6695 GalvaKnights** — the only team that writes real unit tests (4 Kotlin suites with assertions), though CI runs `spotlessCheck`, not the tests. The rarest marker in the set, sitting on an otherwise baseline codebase. - **2102 Team Paradox** — the strongest simulation profile (physics sims across six mechanisms plus maple-sim and replay vendordeps) on an otherwise mid-tier architecture. ## Caveats — read these before you quote the number The 0.55 is honest, but it is a small, imperfect study, and it deserves to be reported with its limits attached. 1. **Correlation, not causation, and a likely confound.** That D8 (sustainability / program maturity) tops the list suggests a lurking variable: older, better-resourced programs have both better software hygiene *and* better results. Team age (rookie year) plausibly drives both. The rubric measures code, not budget, mentorship, or build quality — all of which move EPA. 2. **Small n, mixed game years.** Twenty-four teams across four FRC seasons (2023–2026). Normalized EPA and within-season state percentile are designed to be comparable across years, which justifies pooling, but the games differ. A 2025/2026-only cut (n = 18) gives a materially similar ranking of dimensions. 3. **Repos are shallow clones with `.git` stripped**, so D8 is artifact-only — no commit history, CI run logs, or release discipline — and should be read as a floor. D4 is near-constant and its correlation is statistically weak. 4. **EPA measures the whole robot and its alliance context**, not the software. A great drivetrain controller can't compensate for a slow intake or a no-show alliance partner. 5. **The per-dimension ordering is suggestive, not stable.** At n = 24 the confidence interval on a single Spearman ρ spans roughly ±0.3, so "D8 beats D1" is a reasonable reading of this sample, not an established ranking. 6. **Scoring was not blinded to team identity.** The scorer knew which team's repo was open, and for the LLM-confirmed pass a model may carry knowledge of team reputations — either can inflate the confirmed pass's apparent validity. 7. **Only teams with public repos are scoreable.** That is a selection filter: teams that publish code may differ systematically from teams that don't, in ways that touch both code quality and results. The takeaway is not "write clean code and you will win." It is narrower and more defensible: the dimensions that either put points on the board (D6, D7) or index a mature, well-resourced program (D8) track results; the dimensions that are pure internal engineering investment (D3, D4) do not track a single season's standings — which is exactly what you'd expect, and exactly why you should still do them. The architecture in this book is designed to make D6 and D7 *cheap and reliable* to build, and to make D8 a byproduct rather than a chore. *Performance data: Statbotics v3 API (api.statbotics.io), season-matched per team, retrieved June 10, 2026.* # 35. The Patribots, four years https://frc-code-scout.jointheleague.org/scoring/35-the-patribots-four-years/ **One team, four seasons, a clean monotonic climb — 5.0 → 10.0 → 17.5 → 20.0 — and one number that never moved.** (One asterisk on the series up front: 2024's 17.5 is the repo's *final* state, reflecting an offseason rebuild — the robot that actually competed that season scored closer to ~12; the 2024 section below has the commit evidence.) This is the worked longitudinal case study: FRC 4738, the Patribots, scored against the full [D1–D8 rubric](33-the-rubric.md) across RapidReact 2022, ChargedUp 2023, Crescendo 2024, and Reefscape 2025. Unlike the corpus survey — shallow clones with `.git` stripped — these are full clones with complete commit history, which is what lets us say not just *what* the code became but *when* it became it. Every level below was confirmed by opening the files behind the grep hits; presence alone was never enough. Read down a column and you see a team improving. Read across the years and you see two rules illustrated on the same repo: **rewrite in the offseason**, and **great code can still have a glaring gap.** ## The four-year scoresheet | Season | D1 Arch | D2 Coord | D3 Sim | D4 Test | D5 Log | D6 Auto | D7 Vision | D8 Sustain | **Σ /32** | |---|---|---|---|---|---|---|---|---|---| | **2022** RapidReact | 1 | 0.5 | 0 | 0 | 1 | 1 | 1 | 0.5 | **5.0** | | **2023** ChargedUp | 1.5 | 1 | 0 | 0 | 1 | 2.5 | 2 | 2 | **10.0** | | **2024** Crescendo | 3 | 2 | 1.5 | 0 | 3 | 3 | 2.5 | 2.5 | **17.5** | | **2025** Reefscape | 3 | 3 | 2 | 0 | 3 | 3 | 3 | 3 | **20.0** | One row differs from [the San Diego scoresheet](34-the-san-diego-scoresheet.md), which lists 2025 at Σ = 19.5 with D2 = 2.5: this full-history read confirmed the Superstructure's guarded transitions (described under 2025 below) and raised D2 to 3, for Σ = 20.0. The score doubles, then nearly doubles again, then settles — a textbook trace up [the maturity ladder](../appendices/how-we-developed-this/03-the-maturity-ladder.md). Six of eight dimensions rise. One column — **D4, testing — reads 0 for all four years.** Hold onto that; it is the whole moral of the second half. ## What each season actually was **2022 RapidReact — 5.0 — pre-framework.** Not command-based at all. `Robot.java extends TimedRobot` with hand-rolled device wrappers — `Motor`, `MotorGroup`, `Falcon`, `Gamepad`, `Turret` — and joystick values flowing straight to motor groups inside the periodic loop. No `SubsystemBase`, no command composition, no README, no CI. The one forward-looking piece is a Limelight-aimed turret (`tx`/`ty` targeting across fifteen files) — targeting only, no pose estimation. A small team (four contributors, ninety-two commits) brute-forcing a working robot. This is the baseline everything else is measured against. **2023 ChargedUp — 10.0 — custom framework, strong on the field.** Still not WPILib command-based — zero `SubsystemBase`, zero `CommandBase`. The telling move is in the *first week* of the season: "Convert command to iterative" and "Refactor DriveSubsystem.java to Swerve.java — Delete RobotContainer.java." They started from a command-based swerve template and deliberately tore out WPILib's command framework to run their own iterative architecture, organized into `hardware/`, `calc/`, and `auto/` packages. What is striking is how much capability they reached *without* the modern scaffolding: PhotonVision AprilTag pose estimation feeding `addVisionMeasurement` into a `SwerveDrivePoseEstimator`, dual-camera ambiguity comparison for rejection, drive-to-pose auto-alignment, forty-five PathPlanner paths driven by `AutoBuilder`. Logging is still `SmartDashboard`-only; no sim, no tests. README and a build CI arrive. The story of this repo is good results on a foundation they were about to abandon. **2024 Crescendo — 17.5 — two robots in one repo.** This is the discontinuity — but the commit history shows the score is *half in-season, half offseason rebuild, and the rubric scores the final state.* During the actual competition season (January–April) this repo was command-based on the **Monologue** logging library with **no IO layer**. Then, entirely in summer, the inflection: `2024-07-30` "switch to loggedrobot and basic setup before big io division," `2024-08-02` "ampper adapted to adv kit," `2024-08-21` "gyro???" — the first `GyroIO` interface. The IO-layer + AdvantageKit architecture that earns the D1/D5 = 3 was built **after the season, in July–August 2024**, on the same repo. So 17.5 is the repo's *final* state; the 2024 *competition* robot scored closer to ~12. In its final form it carries per-subsystem `*IO` interfaces across all eleven mechanisms (`GyroIO`, `ShooterIO`, `PivotIO`, `IntakeIO`, `IndexerIO`, `ClimbIO`, `ElevatorIO`, `AmpperIO`, `LimelightIO`, `PicoColorSensorIO`, `MAXSwerveModuleIO`), full AdvantageKit `@AutoLog` inputs, and its autonomous high-water mark — PathPlanner *plus* Choreo actually wired *plus* `LocalADStar` on-the-fly pathfinding. Coordination is command-manager classes (`PieceControl`, `ShooterCmds`, `ShooterCalc`), richer than plain composition but not yet a guarded state machine. **2025 Reefscape — 20.0 — consolidation and a real coordinator.** Refinement of the 2024 platform rather than another leap, and it *opens* already modern — the `2025-01-10` commit is "add claw with akit and logged constants." The IO seam matures into **dual real implementations**: `*IOKraken` and `*IONeo` for the same interface across elevator, wrist, climb, coral/algae claw, and module — clean hardware portability, one interface, two motors. The season's intellectual work is coordination: "add superstructure control" (`2025-01-12`) and a deliberate "start state implementation" (`2025-02-12`) that becomes a genuine `Superstructure`. `SuperState` objects bundle a robot-wide goal into per-subsystem states; a `targetState` is the requested intent, and transitions are **guarded** — `() -> elevator.atPosition(...)` conditions, `waitUntil` gates — so intent is separated from execution. That is the D2 = 3 anchor. Vision reaches D7 = 3: two Limelights behind a `VisionIO` interface, MegaTag2, dynamic std-dev tuning and rejection. Fewer commits than 2024, because less was being invented — a refinement year on a stable base. ## The leap happened in the offseason Every season shares the same heartbeat: a January kickoff spike, heavy February–March build-and-compete, an April taper, then a fall bump that is *not* rewrite work but **Beach Blitz** — the San Diego offseason event, all "day 1 bb" and tuning commits. The architecture decisions, by contrast, happen at kickoff or in deep summer. From 2022 to 2023 the Patribots improved *within* a custom, non-command-based framework — better vision, better autos, the same architectural ceiling. The jump to elite-track scores came from a clean-sheet adoption of the IO-layer + AdvantageKit stack in **July–August 2024** — between the Crescendo season and the Reefscape season, on the Crescendo repo, *not* during any build season. That is why Reefscape 2025 opens already on AdvantageKit with logged constants: the foundation was a finished offseason project *before* kickoff. The pattern across four years is unambiguous — **architecture is decided when there is no game to play, and refined under competition pressure.** They are a team that paid the architectural cost most teams never pay, and paid it in the summer. This is the concrete proof behind the [foundation-first](../appendices/how-we-developed-this/05-foundation-first.md) rule: you do not rewrite your framework in week three of build season; you do it in July. ## The four-year zero Now the gap. Run `git log --all -S"@Test"` across every branch of every season repo and it returns nothing. Not one unit test has ever been written — the D4 = 0 column is not a snapshot, it is a four-year fact, and D4 is the single rarest marker in the entire San Diego corpus, the clearest signal of a real software-engineering culture. The cruelty of it is that Patribots own the *exact* infrastructure that makes testing cheap. The elite-track architecture frames advanced capability as dividends that attach to three seams — the **IO seam** (D1), the **coordination seam** (D2), and the **state seam** (D7/RobotState). They cut the IO seam in 2024 and the coordination seam in 2025. Their `@AutoLog` inputs structs and per-subsystem IO interfaces are precisely what makes unit testing "a deferred dividend you populate, not a refactor you rebuild." They built the plumbing and never opened the tap. The simulation story is the same gap in a second form: D3 has never cleared ~2, stuck at a generic `DCMotorSim` buried in the motor wrappers, instead of mechanism physics in a proper `XxxIOSim`. These are not two problems. They are one — both attach to the IO seam that already exists. ## Prioritized next steps The retrospective ends where every good one should: with a short, ordered list of the highest-leverage moves, chosen so the top item is the cheapest and rarest. 1. **Collect the testing dividend (D4: 0 → 2).** The move. Add a real `XxxIOSim` for *one* mechanism — start with the elevator or wrist for clean 1-DOF physics — using WPILib `ElevatorSim` / `SingleJointedArmSim` inside `updateInputs`, looping *above the line* so sim and real share one controller. Write *one* test that constructs the subsystem with its `IOSim` and asserts it reaches a setpoint. Then change one line of `gradle.yml` so CI runs `test`, not just `build`, gating merges — which simultaneously pushes toward D4 = 3. 2. **Promote simulation from the motor wrapper to the IO layer (D3: 2 → 3).** Same seam, same work as step 1: filling `XxxIOSim` with mechanism physics *is* the simulation upgrade. Then add a `SIM`/`REPLAY` mode switch at the single subsystem-construction point and get AdvantageKit replay of real matches for free — a path to D5 = 4. 3. **Build the state seam — a `RobotState` world model (D7: 3 → 4).** The one seam they have not cut. Today the pose estimator lives privately inside `Swerve`. Extract a dedicated `RobotState` that owns the `SwerveDrivePoseEstimator` (later a pose history and game-piece state); subsystems feed it, vision corrects it, pathing and decisions read from it. That is the difference between "pose estimation exists" and "a world model *is* the architecture." **When:** their own history says summer — the IO layer was a July–August project, the Superstructure a kickoff project. The summer of 2026 is the slot, and unlike 2024 this one needs no demolition, only filling seams that already exist. **If they do exactly one thing:** write the first unit test. It is the rarest, longest-standing gap — four years and zero attempts in the entire history — it requires the least new architecture, and it is the clearest available signal that 4738 is an engineering program and not just a strong-results team. ## Why this belongs in the book Use it as a template. Any team can produce this document about itself: pull full history, score each season honestly against the rubric by *opening the files*, read the scoresheet across time instead of down a column, and let the commit log — not memory — say when the sophistication actually landed. Do that and you will likely find the same two truths the Patribots demonstrate. First, the leap you are proud of probably happened in an offseason, because that is the only time there is room to rewrite a foundation. Second, a genuinely strong codebase — 20 out of 32, elite by any measure — can still carry a single glaring zero for four straight years, precisely because nothing on the field forces you to close it. The retrospective is how you find your own uncollected dividend before someone else scores you and points at it. # Appendices https://frc-code-scout.jointheleague.org/appendices/ # Appendices *Reference, method, and worked examples — the material that supports the narrative without interrupting it.* - **Appendix A** — [How We Developed This](how-we-developed-this/) — the method behind Parts I and IV: how the corpus was read, the eight-dimension rubric, the maturity ladder, what the architecture predicts, and the foundation-first build order. - **Appendix B** — [Glossary & naming decisions](glossary.md) — faceplate, component, seam, IO line, `u`/`x`, estimate vs status, and the naming rationale. - **Appendix C** — [Source-document crosswalk](source-crosswalk.md) — every `knowledge/` source mapped to the chapter(s) that absorb it, so nothing is orphaned. - **Appendix D** — [Reviewing for the seams](reviewing-for-the-seams.md) — the architecture-first code review checklist: the five invariants, S0–S3 severity, and principles P1–P10. - **Appendix E** — [The minimal worked example](minimal-worked-example.md) — the four-channel faceplate stacked three altitudes high in the smallest robot that has all the layers: two drive motors, a color sensor, and a planner that stops on the line. - **Lessons from Outside** — [what the broader robotics world treats as table stakes](lessons-from-outside/), and what FRC mostly skips — including the generators chapter. *The rubric in full, the San Diego scoresheet, and the Patribots case study are no longer appendices — they now live together in the [Scoring Elite Code](../scoring/) section. Other advanced topics moved into [Part I ch. 9](../part-1/09-other-advanced-topics.md).* # Appendix A — How We Developed This https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/ # How We Developed This *The method behind Part I — and how to find out where your own program stands.* Part I shows you the architecture. This appendix shows you the workshop it was built in: how the study was actually conducted, the instrument it produced, and how you use that instrument to assess a team — yours or anyone's — against what the corpus shows. It reads in two movements. The first is **method**: how we read 55 season repositories, the index that lets us count a pattern across all of them, and the one rule that makes the findings trustworthy — *score what's used, not what's present.* The second is **assessment**: the eight-dimension rubric that turns "is this code good" into eight separately observable questions, the maturity ladder that tells you which rung a program is actually on and what pain moves it to the next, and an honest accounting of how much any of it predicts winning. If Part I answers *what the architecture is*, this appendix answers *how we know, and how you measure where you are in it.* 1. [How we read the corpus](01-reading-the-corpus.md) — the study end to end, and the rule that makes it trustworthy. 2. [Eight dimensions of sophistication](02-the-rubric.md) — the rubric: what it measures and how to read a profile. 3. [The novice-to-elite maturity ladder](03-the-maturity-ladder.md) — the five-season climb, sequenced by pain. 4. [What the architecture predicts](04-what-it-predicts.md) — the validation against competition results, honestly. 5. [Foundation-first](05-foundation-first.md) — the build order, the deferred dividends, and the one rule that carries it all. # 1. How we read the corpus https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/01-reading-the-corpus/ The architecture in this book was not designed from first principles. It was read out of source code — dozens of public FRC repositories, plus a wider base of Botball, FLL, and WRO programs — and then turned into something you can teach and grade. This chapter is the method, because the method is what makes the rest trustworthy. ## Two questions Everything here sits on two questions. The practical one: *what does sophisticated student robot code actually look like, concretely enough to teach and to grade?* The local one: *how do San Diego's teams stack up against that standard, and does better code correlate with winning?* The standard was learned nationally — from the best teams in the country — and then applied locally. The rubric is San-Diego-applied but nationally-derived. ## The pipeline The work ran in nine stages, from corpus to rubric to ranked survey to a four-year deep dive on one team. | Stage | What happened | |---|---| | 1. Corpus teardown | Read public repos to find the recurring architecture | | 2. Pattern extraction | Named the shared structures: the IO layer, the coordination paradigms, the ladder | | 3. Local inventory | Catalogued every San Diego team with public code (29 FRC orgs, 12 FTC) | | 4. Bulk download | Shallow-cloned and pruned every repo to a code-only corpus | | 5. Rubric design | Turned the patterns into an eight-dimension anchored rubric | | 6. Pilot + refinement | Scored three teams to test the instrument; fixed two scoring rules | | 7. Full scoring + validation | Scored 24 teams, paired with Statbotics EPA, measured what predicts results | | 8. Build spec | Distilled a foundation-first architecture to grow into | | 9. Patribots deep dive | Re-cloned four seasons with full history; scored year by year | ## The samples at a glance The study reads several different samples, and later chapters cite each by size — worth one table so the numbers never blur together: | Sample | What it is | Used for | |---|---|---| | **37 teams** | hand survey of national FRC repos | pattern extraction (stages 1–2) | | **63 teams / 684 repos** | the full bulk clone | the tree-sitter → DuckDB code index | | **55 repos** | season index — one repo per team, latest real season | prevalence counts; rubric scoring unit | | **232 team-years** | every available season repo per team, pooled across the 55 teams | the cross-validated EPA study | | **24 teams** | the scored San Diego set | the scoresheet + correlation study | | **1 team** | FRC 4738, four seasons, full history | the longitudinal case study | Note the distinction in the middle rows: rubric *scoring* uses each team's most recent season repo, while the EPA *cross-validation* pooled every available season repo per team — which is how 232 team-years coexist with a 55-team, one-repo-per-team scoring unit. Stages 1–2 produced no scores — only a shared vocabulary of observable structures. Three teardowns established it: a broad baseline of 21 non-FRC repos (every program converges on the same three layers — device map, motion primitives, mission logic), a survey of 37 FRC teams across four languages and six coordination paradigms, and a deep dive on the IO layer (FRC's house name for the Strategy pattern). A deliberate cost is baked into stage 4. Clones are shallow and `.git`-stripped, which keeps the corpus small but erases commit history. That is why the sustainability dimension is read as a floor, and why the Patribots deep dive (stage 9) re-cloned with full history to see what the survey could not. ## The golden rule: score what's used, not what's present The single rule that governs all scoring: **every candidate is confirmed by opening the file.** A Choreo vendordep with no referenced trajectories is not Choreo adoption. An empty `src/test` folder is not testing. A class named `Superstructure` that only holds references is baseline command-based wearing a level-3 name. This is not fastidiousness — it is measured. On the same 55 teams, scores from a model that *opened the files* predict EPA at Spearman ρ ≈ 0.53; a mechanical pass that scores grep hits as presence reaches only ρ ≈ 0.29. Confirming use roughly doubles the rubric's predictive validity. The cheap pass is a lead sheet, not a score. The grep matters less than people expect in another way too: agreement between the mechanical pass and the confirmed score is high for testing, architecture, and simulation (κ ≈ 0.8), but low for autonomous, vision, and sustainability (κ ≈ 0.6) — exactly the dimensions where "present" diverges most from "used." Spend the reading budget there. (These are rounded; the canonical per-dimension κ table is in [the rubric in full](../../scoring/33-the-rubric.md).) ## What to carry forward Read the best code first to learn what good looks like; name the structures before scoring anything; confirm every claim by reading. Two touchstones anchor everything that follows — the command-based baseline ([ch. 1](../../part-1/01-baseline-and-shape.md)) and the eight-dimension rubric ([the next chapter](02-the-rubric.md)). # 2. Eight dimensions of sophistication https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/02-the-rubric/ The patterns from the corpus become useful only when they become measurable. The instrument is a rubric of **eight dimensions, each scored 0–4** against anchored, observable indicators, with half-steps allowed. The anchors themselves, the grep/AST cheat-sheet, the measured prevalence table, and the full catalog of profile shapes all live in one place — [the rubric in full](../../scoring/33-the-rubric.md). This chapter is the part that page doesn't carry: where the instrument came from, and why it is shaped the way it is. ## The eight dimensions | | Dimension | The question it asks | |---|---|---| | **D1** | Hardware decoupling (architecture) | How far is subsystem logic separated from physical devices? | | **D2** | Coordination & decision logic | How does the robot decide, and keep mechanisms from fighting? | | **D3** | Simulation | Can the code run, and surprise you, without the robot? | | **D4** | Testing & verification | Are there real, run, asserted tests? | | **D5** | Logging & diagnostics | When the robot misbehaves, how do you know why? | | **D6** | Autonomous & path planning | Authored paths, optimal trajectories, reactive avoidance? | | **D7** | Localization & vision | What does the robot believe about where it is? | | **D8** | Sustainability & process | Will the codebase survive its seniors graduating? | Each dimension runs the same arc — level 0 is absence, level 1 the baseline, levels 2–3 real adoption, level 4 library-grade — because the corpus teardown kept finding that arc, dimension after dimension. The anchors were written by naming what the level *observably looks like* in a repo, not what it aspires to. ## Why dimensions, not a single ladder score The instrument did not start as a rubric. It started as the [maturity ladder](03-the-maturity-ladder.md) — a single ordered climb — and the ladder is genuinely right about sequence *within* a dimension: the IO layer is what makes a subsystem testable, so tests cannot precede it. But the moment the ladder met real repos, it broke as a measuring stick, because teams adopt unevenly *across* dimensions — a clean IO layer with zero tests, AdvantageKit logging on tangled coordination, Choreo trajectories over dead-reckoned odometry. A single rung number averages those into mush, and the average hides precisely the fact a coach needs: which adjacent step a team has already paid for and is not taking. So the design decision was to score each dimension independently, report the vector, and read the **shape** of the profile rather than the total. The sum is still reported; the profile is the finding. The same decision corrected the teaching ladder in one place: the progression plan bundles "IO layer + simulation + lightweight logging" as one leap and "FSM + tests + replay" as another, which is the right *teaching* order but the wrong *measurement* — in the wild those bundles decompose, so the rubric splits them. ## What the pilot changed Before the full survey, the rubric was piloted on three teams chosen as a deliberate spread — likely-high, mid, baseline — to test whether it discriminates and surfaces distinct profiles. It did, and the pilot fixed two scoring rules that the anchors alone underdetermined: **D1 takes the lenient reading** (a maintained 254-style generic base counts toward the top of D1 even without a Real/Sim IO swap — generalization is the achievement, not the naming convention), and **D8 credits a team library that demonstrably exists**, with a note where it is demonstrably consumed. Both calls are recorded on [the San Diego scoresheet](../../scoring/34-the-san-diego-scoresheet.md), which inherited them. ## Why the anchors are calibrated against prevalence An anchor is only meaningful relative to how common its marker is: a structure present in 3 of 55 teams is a ceiling signal, one present in 45 is table stakes, and confusing the two turns the rubric into either flattery or impossibility. That is why every marker in the cheat-sheet carries its measured corpus count — the numbers live in [the prevalence table](../../scoring/33-the-rubric.md#corpus-prevalence-measured). The counts also guard against naming traps the corpus exposed: hardware implementations are named *by device*, not "Real," so grepping `*IOReal` misses most IO layers — the robust signal is an `interface *IO` with two or more implementations, one of them a sim. The general rule — [score what's used, not what's present](01-reading-the-corpus.md#the-golden-rule-score-whats-used-not-whats-present) — applies hardest exactly where naming and presence mislead. Two smaller decisions round out the design. **Equal weighting** is deliberate: an EPA-optimal re-weighting of the eight dimensions was tested and was not distinguishable from equal weight, so the sum stays simple. **Half-steps** exist because real teams sit between anchors; the rubric's scoring rules include an example of a legitimate 2.5 versus a lazy one. ## Reading the profile Scored vectors fall into recognizable shapes — balanced climber, architecture-without-verification, tooling adopter, template inheritor, legacy program, verification ceiling — and the shape, not the sum, determines the highest-leverage next step. The full catalog with its interventions is in [the rubric in full](../../scoring/33-the-rubric.md#reading-the-profile-common-shapes); the design point here is simply that the rubric was built so that its *output* is a diagnosis, not a grade. With the instrument's rationale in hand, the next chapter returns to the ladder the rubric was carved from — [the novice-to-elite maturity ladder](03-the-maturity-ladder.md). # 3. The novice-to-elite maturity ladder https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/03-the-maturity-ladder/ The architecture is a destination; this chapter is the route. A program climbs to it over four or five seasons, and the climb is governed by three rules that matter more than any pattern. ## The principle that governs everything What separates the elite teams in the corpus is **not** that they know the fanciest pattern. It is three habits that make every pattern learnable: 1. **They simulate** — so they can write and verify code without the robot. 2. **They review each other's code** — so quality survives a bad night and a graduating senior. 3. **They retain knowledge across classes** — so year 4 is not year 1 again with new students. The architecture is the *vehicle* for teaching those habits, not the goal. Teach the IO layer to a team that doesn't review code and you get cargo-cult ceremony. So every phase below pairs an **engineering leap** with a **team-process leap**, and the process is the one that compounds. Two rules govern the whole arc. First: **you rewrite in the offseason, never during build season.** Each architectural leap is a radical change; the only safe time is May–December, practicing on last year's game where you already know what "working" looks like. A team that tries to learn the IO layer in February loses the regional. You can watch this rule play out in one team's four-year git log in [the Patribots, four years](../../scoring/35-the-patribots-four-years.md), whose elite-track rebuild landed in an offseason, not a build season. Second: **sequence by pain, not by prestige.** Each rung must be motivated by a problem the team has actually hit. Adopt a pattern early because elite teams have it and you drown in boilerplate you have no use for. ## The five phases | Phase / Season | Engineering leap | Motivating pain | Team-process leap | |---|---|---|---| | **1** | Command-based + closed loop; vendored swerve | "It barely drives" | git; one software lead | | **2** | IO layer + simulation + lightweight logging | "Can't test — no robot time" | PR review; onboarding doc; pairing | | **3** | Wanted/current FSM + superstructure; unit tests; replay | "Mechanisms fight; can't debug" | subsystem ownership; rookie curriculum | | **4** | Choreo + repulsor planning; extract team library; 2nd language | "Need speed; rewriting the same code yearly" | students run review; written standards | | **5+** | State graph / behavior tree; (capstone) message passing | "Transition logic outgrew the FSM" | process is self-sustaining | **Phase 1 — a robot that reliably works.** Not good code — a robot that drives, runs a mechanism or two, and completes a simple autonomous in clean command-based WPILib. The core lesson is the open-loop-to-closed-loop leap: *drive until the sensor says stop, not until the timer says stop.* Use a vendored swerve library so the team gets a modern drivetrain without building the abstraction they haven't earned yet. **Phase 2 — write code without the robot.** This is the phase that quietly creates an elite team. The universal pain: one robot, ten programmers, and it's on the cart getting its swerve rebuilt. The leap is the IO layer — dependency inversion, a real CS concept — so the whole subsystem runs on a laptop. Add lightweight logging (DogLog/Epilogue), not full replay; that's ceremony they haven't earned. The process leap matters more: nobody merges their own code. **Phase 3 — intent vs execution, and proof.** Mechanisms now fight, and "what is the robot doing" gets hard. Two leaps: a wanted/current state machine with a superstructure (teach the centralized version first — it's more readable), and unit tests, the payoff the IO layer was always promising. This is the first year the team's knowledge clearly outlives any one student. **Phase 4 — become a codebase, not a project.** Optimize and consolidate: separate the three path concerns (PathPlanner, Choreo, repulsor planning), and extract a team library carried season to season — the structural signature of a *program* rather than a team. *If the team can survive its best programmer graduating, you have made it.* **Phase 5 — the frontier, only if it earns its keep.** The honest note: a clean Phase 4 codebase is already elite. Most powerhouse teams are a polished FSM-plus-superstructure-plus-replay, not something exotic. Reach for a state graph or behavior tree only when the decision logic genuinely outgrows a switch statement. ## The risk to manage The temptation is to skip ahead — adopt AdvantageKit's IO layer in Phase 1 because elite teams have it, and drown in boilerplate with no felt need behind it. Every rung must be motivated by a pain the team has actually hit. The three habits — simulate, review, retain — are what carry a team across the graduation cliff that kills most programs around year 3. Whether any of this actually correlates with winning is the question [the next chapter](04-what-it-predicts.md) tests. # 4. What the architecture predicts https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/04-what-it-predicts/ A rubric is only worth trusting if its scores mean something. So the scores were tested: 24 San Diego teams scored on the eight dimensions, each paired with season-matched Statbotics EPA (a normalized strength rating built from match results), and the two correlated. The answer is honest rather than triumphant. The full per-team scoresheet and the raw correlation tables are [the San Diego scoresheet](../../scoring/34-the-san-diego-scoresheet.md). ## The headline: moderate, not decisive Code sophistication correlates **moderately** with competition results — rubric total versus normalized EPA is Spearman ρ ≈ 0.55. Better software is associated with better results, but it explains only about a third of the variance. The full per-team scoresheet and the per-dimension correlation table live in [the San Diego scoresheet](../../scoring/34-the-san-diego-scoresheet.md); what matters here is the shape of the result, not the rows. The per-dimension split has a clean reading. The signal concentrates in D8, D6, and D7, and nearly vanishes in D3 and D4. D6 and D7 put points directly on the board — a team that runs real autos and aligns to targets with vision scores more, immediately. D8 (CI, a carried library, docs, a formatter) proxies overall program maturity, which sustains competitiveness across a season. D3 and D4 — simulation and testing — are internal engineering investments whose payoff is robustness and developer velocity, not raw match points; teams adopt them for reasons mostly orthogonal to a given season's standings. (D4 is also near-constant — almost every team scores 0 — so its low correlation is partly a low-variance artifact, not evidence that testing hurts.) Because the link is loose, the mismatches carry most of the lesson — the sophisticated codebase that finished in the bottom third of the state, and the minimal repo that posted a 72nd-percentile EPA. Those outlier profiles are worked through in [the scoresheet chapter](../../scoring/34-the-san-diego-scoresheet.md#the-outliers-are-the-interesting-part); the conclusion they force is that code sophistication and competition performance are different axes. The rubric measures the first; it does not pretend to measure the second. ## The confound, stated plainly The most important caveat is that **most of what predicts EPA from "code" is size and program age, not engineering quality.** A model of raw code features hits ρ ≈ 0.58 — but codebase size plus program maturity *alone* scores ≈ 0.60, while the sophistication features with size removed score ≈ 0.38, and a within-team view collapses to ≈ 0.05. Bigger, older, better-resourced programs have both more code and better results. That D8 (program maturity) tops the dimension list is the same lurking variable showing through. Do not read a high correlation as "good code wins," and do not reward sheer volume. Other limits are real: n = 24 across mixed game years; shallow `.git`-stripped clones make D8 a floor; EPA measures the whole robot and alliance context, not the software. A great controller can't compensate for a slow intake or a no-show partner. ## What only the history could show The validation across teams is a snapshot; the four-year deep dive on the Patribots is the time-series. Re-cloned with full commit history and scored season by season, they show a clean monotonic climb — Σ ≈ 5 → 10 → 17.5 → 20 (2024's 17.5 is the repo's final, post-rebuild state; the robot that actually competed that season scored closer to ~12). The finding only the history could reveal: the leap to elite-track scores happened **in the 2024 offseason, not in any build season.** The 2024 competition robot ran on simple logging with no IO layer; the IO-layer/AdvantageKit rebuild landed in July–August. That is [the "rewrite in the offseason" rule](03-the-maturity-ladder.md) showing up in a real team's git log. The persistent gap across all four years, verified against every branch: not one unit test has ever been written — which became the team's single highest-leverage next step. The full four-season deep dive, with the year-by-year scoresheet and the commit evidence, is [the Patribots, four years](../../scoring/35-the-patribots-four-years.md). The honest conclusion: the architecture is worth building because it makes a program *faster and more durable*, and it modestly tracks results — but it is not a substitute for a good robot. The next section turns from "is it true" to "how do you build it," starting with [the foundation-first order](05-foundation-first.md). # 5. Foundation-first https://frc-code-scout.jointheleague.org/appendices/how-we-developed-this/05-foundation-first/ [Chapter 2](../../part-1/02-five-views.md) said *what* the architecture is. This chapter says *in what order you build it and why that order is safe.* The organizing idea: **build the seams, defer the payoffs.** Every advanced capability attaches to one of the three seams, so if you cut the seams correctly in week one, each later feature is an *addition at a named point* rather than a refactor. ## Each rung attaches to a seam The progression is a map, not a wish list. Every rung names the seam it plugs into and roughly how much new code it costs. | Rung | Capability | Attaches to | New code | |---|---|---|---| | 1 | Mechanism physics in sim | fill `XxxIOSim` | a WPILib sim inside the Sim impl | | 2 | Unit tests | construct subsystem with `XxxIOSim` | `@Test`: step sim, assert | | 3 | Vision pose fusion | `RobotState.addVisionMeasurement` + `VisionIO` | a camera impl + std-dev tuning | | 4 | Authored autos | Superstructure goals as named commands | PathPlanner paths/autos | | 5 | Time-optimal trajectories | swap the path source | Choreo where time matters | | 6 | On-the-fly pathfinding | reads `RobotState.getPose()` | pathfind-to-pose | | 7 | Deterministic log replay | `REPLAY` run mode (already wired) | **none in subsystems** — flip the mode | | 8 | Smart coordination | replace `applyGoal` body | guarded table → planner → state graph | | 9 | Versioned team library | promote `util/` + generic IO base | extract, version, consume | Nothing in rungs 1–9 modifies the seams; they only fill them. That is the whole promise: a team that cut the seams can climb this ladder without the offseason rewrites that sink most programs. ## The dividends teams skip The deferred-dividend rungs — sim (1), tests (2), and replay (7) — are the ones teams build the seam for and then never collect. Filling `IOSim` costs an afternoon and unlocks both testing and replay, yet the corpus finding is blunt: almost every team that builds the IO seam never writes the test or flips the replay mode. Collecting them is the clearest marker of real software culture, and the foundation already paid for them. ## The build order The architecture has a definition-of-done sequence. Steps 1–6 are the foundation; 7–10 are the climb, and none of them touches the seams: 1. **Command-based skeleton** — *done when:* teleop drives. 2. **IO seam on Drive** — interface, inputs struct, real/sim/replay impls. *Done when:* it runs in SIM with a stub and doesn't crash. 3. **`RobotState`** — pose estimator fed by drive odometry. *Done when:* the pose shows on the field in sim. 4. **Logging facade** — every subsystem's inputs published. *Done when:* every mechanism is visible in AdvantageScope. 5. **One mechanism, full quartet** — e.g. an elevator with sim physics. *Done when:* a unit test steps the sim and asserts it reaches a setpoint. 6. **Superstructure** — goal enum plus one real interlock. *Done when:* an illegal request is provably sequenced safely. 7. **Vision** → 8. **Autos** → 9. **Collect the dividends (replay, broader tests)** → 10. **Extract the library** once patterns have repeated three times. ## Tools attach at seams, too The architecture is tool-agnostic *at defined seams*, so picking or swapping a tool is a localized change: PhotonVision or Limelight is a `VisionIO` impl into `RobotState`; YAGSL or CTRE Tuner X is a `DriveIO` impl; maple-sim lives inside `IOSim`; AdvantageKit or DogLog is a logging backend; Choreo swaps PathPlanner where seconds decide matches. AdvantageScope sits on top of all of it as the viewer. ## The one rule that carries it A vendor type — a `TalonFX`, a `PhotonCamera` — must **never appear in a subsystem, a command, or the superstructure.** Only inside an IO implementation. If a `com.ctre` or `org.photonvision` import shows up above the IO line, the seam has leaked and a tool swap has become a refactor. This is the discipline teams actually skip. Of the 24 corpus teams that built an IO seam, **22 still import a vendor type above the line.** Building the interface is the easy 80%; keeping vendor types out of everything above it is the unglamorous 20% almost no one finishes — which is exactly why it deserves a lint rule rather than good intentions, and why clean vendor confinement is a distinguishing top-tier marker rather than a given. That confinement rule, generalized, is also where Part III begins. For what the broader robotics field treats as table stakes that this architecture still skips, see [Lessons from Outside](../lessons-from-outside/01-lessons-from-outside.md). # Appendix B — Glossary & naming decisions https://frc-code-scout.jointheleague.org/appendices/glossary/ **Every load-bearing term in this book earns its place, and several were chosen against plausible alternatives that were already spent.** This appendix is two things at once: a glossary of the terms the architecture depends on, and a record of the naming decisions behind the few that were contested. The rule that governed those decisions is the same one that governs the code — *a name must survive a change of reader, and you do not import a word the destination domain has already spent.* Read the definitions to understand the vocabulary; read the rationale entries to understand why the vocabulary is what it is. ## Core terms **Component.** The informal noun for any active thing on the robot — a motor, a sensor, a subsystem, `RobotState`, the superstructure. Deliberately lowercase and never a type: there is no `Component` supertype, only the shared shape below. See [the Portable Component Model](../part-3/25-portable-component-model.md). **Faceplate.** The one recurring shape: the four-channel interface every component presents, like a rack module's front panel — the same jacks in the same places no matter what circuitry sits behind them. A component takes `Config` once, then each tick runs a pure step `(State′, Command_out[]) = update(Command_in, Observations)`. A motor, a subsystem, and the superstructure present the same faceplate — they differ only in which of the four channels they populate and in whether their children are motors or subsystems. The robot is a tree of components, commands flowing down and state flowing up. *Faceplate* is a word of the book's prose, never a name in the code: the concrete types keep their natural names, and the faceplate is the shape they all share. See [the Portable Component Model](../part-3/25-portable-component-model.md). **Seam.** A deliberate boundary in the code where one concern is cut off from another so each side can change without disturbing the other. The architecture is built on three of them. **The three seams.** The whole foundation reduces to three cuts: - **The IO seam** — between a subsystem's logic and its physical devices (the hardware boundary). - **The state seam** — between where a fact is *measured* and where it is *used*, mediated by a central world model (`RobotState`) rather than read directly off sensors. - **The coordination seam** — between a subsystem's job (*run this mechanism well*) and the robot's job (*decide what all the mechanisms should be doing*), owned by the `Superstructure`. See [the IO seam](../part-1/03-the-io-seam.md), [the state seam](../part-1/04-the-state-seam.md), and [the coordination seam](../part-1/05-the-coordination-seam.md). **IO line / IO seam.** The line between a subsystem's logic and its hardware. Above the line lives mechanism intent and (often) the control loop; below it live the concrete device drivers. Its defining rule is vendor confinement (below). "IO seam" names the *boundary*; "IO line" is the same boundary viewed as a wall you must not let vendor types cross. See [hardware abstraction and the IO line](../part-2/16-hardware-abstraction.md). **Fill-pattern taxonomy.** How components are classified: not by a type hierarchy but by *which of the faceplate's channels they populate* — a sensor emits `State` but takes no `Command`, an actuator the reverse, an estimator turns `Observations` into an estimate, an executive's output is mostly status. See [the Portable Component Model](../part-3/25-portable-component-model.md). **RobotState.** The central world model — the single object every subsystem writes its measurements into and every consumer reads its estimates out of, so no one reads a raw sensor twice. In faceplate terms it is an *estimator*: a component that takes observations in and emits a fused estimate, "a sensor that does work." See [the state seam](../part-1/04-the-state-seam.md). **Superstructure.** The coordination component — the one place that decides what every subsystem should be doing, enforcing interlocks and sequencing so no button-binding has to. It is structurally the *same kind of component* as a subsystem; it differs only in that its `Command_out` feeds subsystems rather than motors. See [the coordination seam](../part-1/05-the-coordination-seam.md). ## The IO layer vocabulary **IO layer (location) vs hardware abstraction (property).** "IO layer" names a *location* — where the boundary sits, one interface per subsystem at the logic/device line. "Hardware abstraction" names a *property* — whether the control loop lives above or below that line. See [hardware abstraction and the IO line](../part-2/16-hardware-abstraction.md). **The IO quartet.** The four files that make up one subsystem's IO layer, generated together: - **`XxxIO`** — the interface naming the hardware boundary (e.g. `ElevatorIO`, `GyroIO`, `ModuleIO`). - **`XxxIOInputs`** — the inputs struct (below) the interface fills. - **`XxxIO`** — the real implementation, *named for the device it drives* (below), e.g. `ElevatorIOTalonFX`. - **`XxxIOSim`** — the simulation implementation, backed by a WPILib physics model. **Inputs struct.** A plain, serializable data object (`XxxIOInputs`) that carries everything crossing the hardware boundary *upward* — position, velocity, applied voltage, current, temperature, a `connected` flag. The interface exposes a single `updateInputs(inputs)` method that fills it, rather than a fistful of getters. The reason it exists is logging: because every value crossing the boundary lands in one recordable object, the whole match can be replayed through the real code. See [hardware abstraction and the IO line](../part-2/16-hardware-abstraction.md). **Null-object IO.** The do-nothing member of the IO family — a full, valid implementation whose methods are deliberately empty, so a subsystem with unplugged hardware runs as a safe no-op instead of crashing, and REPLAY has something inert to hold the seam open. Written either as a named class (`ElevatorIONull`, `NoElevator`) or, where the interface's methods all default to empty, as an anonymous `new ElevatorIO() {}` that costs zero files. **Fault.** A detected abnormal condition — a disconnected device, a stale sensor — carried explicitly as part of a component's *status* (and, at the seam, as the inputs struct's `connected` flag), so degradation is a defined state rather than a crash. **Vendor confinement (the vendor-confinement rule).** The single non-negotiable of the IO layer: a vendor type — `com.ctre.*`, `com.revrobotics.*`, `org.photonvision.*` — may appear *only below the IO line*, inside a concrete `XxxIO` implementation. It must never appear in a subsystem, in `RobotState`, in the `Superstructure`, or in any test. This is what makes hardware swaps and sim local to one file. ## Command / state vocabulary **`u` / `x` (Command / State).** The two data channels every component carries, named after the state-space control pair: `u` is the *command* flowing in from above (the setpoint), `x` is the *state* flowing back — `Command` and `State`. The names were chosen to be frame-invariant: they mean the same thing no matter who reads them, at every altitude of the tree. See the naming rationale below. **Config / Command / State / Command_out.** The four serializable channels ("PODs") of the faceplate: - **`Config`** — identity and calibration set (mostly) once per session: CAN IDs, gear ratios, gains, limits, standard deviations. Kept separate from per-tick data, with a runtime door for the rare parameter that must change live. - **`Command`** (`u`) — the per-tick command in, from the component above. - **`State`** (`x`) — the per-tick state out: estimate + status (below). - **`Command_out`** (`u′`) — the commands this component emits *downward* to its children, returned as a value, never pushed as a side-effect. **`Observations`.** The per-tick measurements a component receives from below — its children's `State`, or, at a leaf, raw sensor readings crossing the boundary upward. The second argument of the pure step, alongside `Command_in`. **Goal vs setpoint (and wanted vs current).** A **goal** is a robot-wide requested outcome ("score L4"); a **setpoint** is the numeric target one mechanism holds, derived from the goal by the superstructure. **Wanted/current** (2910's names; *target* and *goal* are synonyms for the first) is the two-enum pattern that keeps the requested state and the state actually in effect separate, with a transition function as the only writer of the second. See [coordination state machines](../part-2/22-coordination-state-machines.md). **Estimate vs status.** The two halves of `State`. The **estimate** is the measured or fused physical quantity — position, velocity, pose. The **status** is what the component is *doing* — its FSM node, current goal, `atGoal`/`isReady` flags, fault state. For a motor the two coincide (its state *is* its measurement); above the leaf they split, and for an executive the status is the *primary* output. ## Run modes & payoffs **Run modes (REAL / SIM / REPLAY).** The three ways the same robot program runs, selected in exactly one place at construction: - **REAL** — the `XxxIO` implementations talk to physical hardware. - **SIM** — the `XxxIOSim` implementations talk to a physics model, so the whole robot runs on a laptop with no hardware. - **REPLAY** — no-op IO holds the seams open while a recorded log feeds the inputs structs back through the unchanged logic, reproducing exactly what the robot decided. **Deferred dividend.** The payoff of the IO seam that does not show up until later: because a subsystem can be built with its `XxxIOSim`, it can be driven to completion in a CI test harness and asserted on — unit testing robot code, which almost no team does and which the IO seam makes mechanically possible. You pay the seam's cost up front; the testability dividend is deferred. ## Naming decisions The following terms were chosen deliberately against alternatives that were already spent in a neighboring domain. Each entry records the choice and why. **Why *faceplate* (and how *block* lost).** The recurring shape needed a noun, and the search taught us two things: most candidates collide, and the thing being named is the *interface shape*, not the unit that presents it. The unit needs no reserved noun at all — lowercase *component* serves, and the concrete types keep their natural names — but the shape needs a word the book can say precisely. It is a **prose-only term**: it never appears as a type or identifier in code, which frees the choice to optimize for the metaphor rather than for how it reads in a signature. - **`Block`** — the original working name, chosen from block diagrams (the exact config-in / ports / internal-state model) as "the least-spent word in application software." That judgment missed the collision that matters most: **FTC's beginner programming language is literally called Blocks**, so for this book's own audience "rewrite your robot as blocks" reads as "go back to visual programming." Add code blocks and blocking calls, and the word fails the rule it was chosen under. - **`Node`** — rejected: graph node, linked-list node, ROS node, k8s node all claim it; maximally overloaded. - **`Unit`** — rejected hard: collides with WPILib's `Units` measure library *in this very codebase*. - **`Module`** — rejected: collides with a swerve **module** and with Java **modules**. - **`Panel`** — rejected: the destination domain has spent it on hardware (the Power Distribution Panel), Java has spent it (`JPanel`), and "elevator panel" confidently reads as a dashboard tab — the worst failure mode, a wrong meaning rather than a vague one. - **`Jack`** — rejected for naming the wrong element: a jack is the socket you plug into, one endpoint of one wire, not the thing that has the sockets. Also a lifting mechanism teams literally build. - **`Channels`** — rejected for self-collision: *channel* already names each of the four members, so the plural can't also name the whole; and in WPILib a channel is a roboRIO port number. - **`Pinout`** / **`Signature`** — the runners-up, both sound: *pinout* is the electronics word for exactly this (a standard arrangement of connections many parts share, unused pins marked NC), and *signature* has the ML-module precedent for structurally-satisfied interfaces. - **`Component`** — kept, but demoted to the informal unit noun rather than the shape's name; ROS 2 uses it the same way for composable nodes. - **Faceplate** — chosen. The rack-module image *is* the concept: every module presents the same jacks in the same places, whatever circuitry sits behind them; wiring is patching one module's output jack to another's input; and the fill-pattern taxonomy reads directly as *which jacks are populated*. The word is essentially unspent in robot code, and because it lives only in prose, its length never costs a signature. **Why `XxxIOTalonFX`, not `XxxIOReal`.** The real implementation is named for the *device it drives*, not for the abstract fact that it is "the real one." `ElevatorIOTalonFX` tells you at a glance what hardware is below the line, and it scales: when a mechanism has a Kraken variant and a NEO variant, or a comp-bot and a practice-bot with different electronics, `…Real` gives you a name collision while `…TalonFX` / `…SparkMax` / `…Comp` stay distinct and self-documenting. "Real" also lies in REPLAY, where nothing is real; the device name never does. **Why `Command` / `State`, not `Input` / `Output`.** The channels are named for what they *are* in control terms (`u` command, `x` state), not for their direction relative to some particular reader. "Input" and "Output" are reader-relative: one component's output is the next component's input, so the words flip meaning as you move through the tree and stop meaning anything stable. `Command` and `State` are frame-invariant — a command is a command whether you are sending or receiving it, and state is state whether you measure it or read it — which is exactly the property a name needs to survive a change of reader across every level of the component tree. See [the Portable Component Model](../part-3/25-portable-component-model.md). # Appendix C — Source-document crosswalk https://frc-code-scout.jointheleague.org/appendices/source-crosswalk/ **This appendix is the machine-checkable record that nothing in the source corpus was orphaned by the wiki rewrite.** Every document under `knowledge/` (and `docs/review/`) maps to the chapter or chapters that absorbed it, with one of three dispositions. **KEPT (reference)** — the file stays in place as a citable reference source, deeper than the wiki chapter that summarizes it. **ARCHIVED** — the file's content was fully absorbed into the wiki and the original moved to `knowledge/archived/`, where each archived file carries a header pointing back to the chapter that now owns it. **TOOL DEP** — the file stays in place because a skill or script reads it directly (scaffold-robot, analyze-team, setup-logging/simulation/testing, `build_site.py`, `agent_score.py`); it is load-bearing tooling, not just prose. | Source document | Absorbed by | Disposition | | --- | --- | --- | | `INDEX.md` | — (the annotated index of `knowledge/` itself) | Not absorbed — tooling/index; read by skills to navigate the corpus | | **build-spec/** | | | | `elite-architecture.md` | [Part I](../part-1/) + [Part II](../part-2/) (ch. 1–23) | TOOL DEP — kept; source for scaffold-robot / analyze-team | | `code-review-principles.md` | [Appendix D](reviewing-for-the-seams.md) | ARCHIVED | | `logging.md` | [Part I ch. 7](../part-1/07-cross-cutting-practices.md) + [Part III ch. 29](../part-3/29-telemetry-replay-tests.md) | TOOL DEP — kept; source for setup-logging | | `simulation.md` | [Part I ch. 7](../part-1/07-cross-cutting-practices.md) + [Part II](../part-2/) | TOOL DEP — kept; source for setup-simulation | | `testing.md` | [Part I ch. 7](../part-1/07-cross-cutting-practices.md) + [Part II](../part-2/) | TOOL DEP — kept; source for setup-testing | | `other-topics.md` | [Part I ch. 9 — Other advanced topics](../part-1/09-other-advanced-topics.md) | ARCHIVED | | `subsystems/00–08` | [Part II ch. 18](../part-2/18-subsystem-archetypes.md) (and ch. 15–23) | KEPT (reference source) | | **corpus-analysis/** | | | | `02-frc-37-team-survey.md` | [Part I](../part-1/) + [Appendix A](how-we-developed-this/) | ARCHIVED | | `03-io-layer-strategy-pattern.md` | [Part I ch. 3](../part-1/03-the-io-seam.md), [Part II ch. 16–17](../part-2/16-hardware-abstraction.md), [Appendix B](glossary.md) | ARCHIVED | | `04-novice-to-elite-progression.md` | [Appendix A ch. 3 — maturity ladder](how-we-developed-this/03-the-maturity-ladder.md) | KEPT (reference) | | `05-motor-io-interfaces.md` | [Part II ch. 17](../part-2/17-motor-interfaces.md), [Part III ch. 26](../part-3/26-portable-motor-interface.md) | KEPT (reference) | | `06-lessons-from-broader-robotics.md` | [Lessons from Outside ch. 1](lessons-from-outside/01-lessons-from-outside.md) | ARCHIVED | | `07-code-generators.md` | [Lessons from Outside ch. 2](lessons-from-outside/02-code-generators.md) | KEPT (reference) | | `08-drivetrain-as-architecture.md` | [Part I ch. 6](../part-1/06-the-drivetrain.md), [Part II ch. 19](../part-2/19-the-drivetrain-subsystem.md), [Part III ch. 27](../part-3/27-portable-swerve-interface.md) | KEPT (reference) | | **rubric/** | | | | `rubric.md` | [Scoring — the rubric in full](../scoring/33-the-rubric.md) + [Appendix A ch. 2](how-we-developed-this/02-the-rubric.md) | TOOL DEP — kept; read by `build_site.py`, `agent_score.py`, scoring skills | | **survey/** | | | | `sd-frc-final-report.md` (+ inventories + `*.csv`) | [Scoring — the San Diego scoresheet](../scoring/34-the-san-diego-scoresheet.md) + [Appendix A ch. 4](how-we-developed-this/04-what-it-predicts.md) | KEPT (reference) | | `sd-frc-scores-pilot.md` | [Appendix A ch. 2](how-we-developed-this/02-the-rubric.md) (the pilot that fixed two scoring rules) + [Scoring — the San Diego scoresheet](../scoring/34-the-san-diego-scoresheet.md) | KEPT (reference) | | **examples/** | | | | `methodology.md` | [Appendix A ch. 1](how-we-developed-this/01-reading-the-corpus.md) | KEPT (reference) | | `sample-score-output-reefscape2025.md` | [Scoring — the rubric in full](../scoring/33-the-rubric.md) (worked candidate-pass example, cited in the scoring procedure) | KEPT (reference) | | `patribots-four-year-scoring.md` | [Scoring — the Patribots, four years](../scoring/35-the-patribots-four-years.md) | KEPT (reference) | | `patribots-four-year-scoring.pdf` | same as the `.md` above (PDF rendering) | KEPT (reference artifact) | | **alternatives/** | | | | `01–04` + `README` | [Part I ch. 8](../part-1/08-alternatives.md), [Part II ch. 23](../part-2/23-coordination-graphs-trees.md) | KEPT (reference) | | **specs/** | | | | `portable-component-model.md` | [Part III ch. 25](../part-3/25-portable-component-model.md) (and ch. 24, 28–31), [Appendix B](glossary.md) | KEPT (reference) | | `portable-motor-interface.md` | [Part III ch. 26](../part-3/26-portable-motor-interface.md) | KEPT (reference) | | `portable-swerve-interface.md` | [Part III ch. 27](../part-3/27-portable-swerve-interface.md) | KEPT (reference) | | **docs/review/** | | | | `portable-component-model-review.md` | [Part III ch. 32](../part-3/32-open-questions.md) | KEPT (reference) | # Appendix D — Reviewing for the seams https://frc-code-scout.jointheleague.org/appendices/reviewing-for-the-seams/ **A code review is the last gate that keeps the architecture honest.** Every other chapter in this book describes how to *build* the three seams — [the IO seam](../part-1/03-the-io-seam.md), the state seam, and [the coordination seam](../part-1/05-the-coordination-seam.md). This appendix is the canonical, enforceable guide for *protecting* them one pull request at a time. It is written for both human reviewers and review agents, and it prioritizes the violations that quietly erode testability, simulation fidelity, replay/debuggability, and long-term maintainability — the exact dividends the seams were built to earn. The core rule that governs everything below is the same rule that governs scoring: > **Review what is *used*, not what is *present*.** A file or class name is not evidence. A vendordep in the build file is not adoption. A `Superstructure` class is not a coordination seam unless it actually coordinates transitions with guards. Open the cited files, read the call graph, and confirm the seam does the work its name claims. --- ## The review contract A review protects five invariants. A change passes only if the code still preserves all of them. See [hardware abstraction and the IO line](../part-2/16-hardware-abstraction.md) and [cross-cutting practices](../part-1/07-cross-cutting-practices.md) for the full definitions. 1. **The IO seam is intact.** Subsystem logic is separated from hardware through `XxxIO` plus `XxxIOInputs`. 2. **The state seam is intact.** One `RobotState` owns fused world state; all other code *reads* state from it. 3. **The coordination seam is intact.** One `Superstructure` (or equivalent) owns cross-subsystem goal transitions and guards. 4. **Vendor confinement is intact.** Vendor SDK types stay *below* the IO line — only inside `XxxIO` and `XxxIOSim`. 5. **Deferred dividends stay possible.** Nothing blocks the later payoffs: D3 simulation, D4 tests, D5 logging/replay, D6/D7 growth. Any change that weakens a seam to make short-term coding easier is **architectural debt, not simplification** — and should be named as such in the review. --- ## Severity levels Use one consistent severity vocabulary across every review so findings are comparable from PR to PR. | Level | Name | Meaning | | --- | --- | --- | | **S0** | Blocker | Breaks an architectural invariant, or makes safe operation unverifiable. | | **S1** | High | Preserves runtime behavior today, but creates a near-term test / sim / replay dead end. | | **S2** | Medium | Local design smell that increases coupling or hides intent. | | **S3** | Low | Readability or consistency issue with low structural risk. | **Default to S0/S1 whenever a change crosses seams incorrectly.** A seam violation that "works on the robot" is still a blocker. --- ## The ten principles Each principle lists what to **enforce**, the **red flags** that signal a violation, and the **evidence of correctness** that clears it. ### P1 — Enforce strict seam boundaries **Enforce** - Subsystems depend on the `XxxIO` contract, not concrete hardware classes. - `RobotState` and `Superstructure` are hardware-agnostic logic layers. **Red flags** - A subsystem imports `com.ctre`, `com.revrobotics`, or `org.photonvision` directly. - `RobotState` imports hardware, camera, or subsystem *implementation* classes. - The `Superstructure` sets motor outputs directly. **Evidence of correctness** - Vendor imports exist only in `XxxIO` and `XxxIOSim`. - The subsystem constructor accepts an interface (`XxxIO`), not a vendor object. ### P2 — Reject leaky interfaces **Enforce** - IO interfaces expose robot-domain intent and measured state, not vendor mechanics. - Interfaces are sufficient for *both* REAL and SIM implementations. **Red flags** - An interface exposes vendor-specific handles or configuration objects. - Interface methods mirror the vendor API surface with no subsystem intent. - A missing `updateInputs(inputs)` call, or an incomplete inputs struct that hides critical state. **Evidence of correctness** - The interface can be implemented by both a hardware class and a physics-sim class. - The inputs struct includes every value needed for control, logging, and diagnostics. ### P3 — Protect abstraction direction **Enforce** - High-level policy depends on abstractions; low-level details depend on hardware. - Control decisions flow in one direction: **goal → coordinator → subsystem → IO → device.** **Red flags** - Commands bypass subsystem APIs and write hardware directly. - Subsystems call into coordinator decisions (a reverse dependency). - "Utility" helpers become hidden cross-layer service locators. **Evidence of correctness** - The call graph follows one direction, from intent to actuation. - Cross-subsystem logic is centralized in the `Superstructure`, not scattered across mechanisms. ### P4 — Keep coordination centralized and guarded **Enforce** - One transition function owns cross-subsystem sequencing and interlocks. - The goal-request API separates *intent* from *execution*. **Red flags** - The `Superstructure` exists only as a container with jog methods. - Interlock logic is duplicated across multiple commands or subsystems. - Goals are represented as ad-hoc booleans spread through the code. **Evidence of correctness** - The coordinator exposes `requestGoal(...)` or an equivalent command API. - Guarded transitions are explicit and testable. ### P5 — Preserve testability as a first-class requirement **Enforce** - New subsystem behavior remains testable with `XxxIOSim`. - Important safety transitions have at least one command-level test path. **Red flags** - New logic requires real hardware to execute at all. - Static singletons or hidden globals prevent constructing isolated units. - There is no deterministic time-stepping path for tests. **Evidence of correctness** - The subsystem can be constructed with sim IO in a unit test. - Tests assert *behavior*, not merely the absence of exceptions. ### P6 — Preserve the simulation-fidelity path **Enforce** - Code supports SIM mode without alternate logic branches that change behavior semantics. - Physics-sim attachment points remain inside the `IOSim` implementations. **Red flags** - SIM mode bypasses normal subsystem logic with special shortcuts. - Mechanism state is fabricated in the subsystem's `periodic()` instead of in IO sim. - Time or sensor updates are tied to wall-clock assumptions. **Evidence of correctness** - The *same* subsystem code path runs in REAL and SIM; only the IO implementation differs. - Sim classes own model update and sensor synthesis. ### P7 — Preserve the logging and replay contract **Enforce** - Inputs are logged at the seam, immediately after `updateInputs`. - Logged data is enough to reconstruct decisions offline. **Red flags** - Only setpoints are logged, not measured inputs. - Logging is sparse — present in only a subset of subsystems. - Replay mode would require subsystem rewrites. **Evidence of correctness** - Structured per-subsystem inputs logging is present consistently. - Coordinator and state outputs are also logged where they are needed for diagnosis. ### P8 — Optimize for future change, not current convenience **Enforce** - Changes reduce or hold coupling; they do not add hidden integration points. - New mechanism code follows the quartet pattern: `XxxIO`, `XxxIO`, `XxxIOSim`, subsystem. **Red flags** - A quick fix adds a direct reference to a sibling subsystem. - A new mechanism skips the IO seam "for now." - Copy-paste patterns repeat where a shared abstraction is due — by the third use. **Evidence of correctness** - The subsystem package could be lifted into a library with minimal changes. - New code extends existing seam patterns rather than inventing side paths. ### P9 — Prefer explicit contracts over implicit behavior **Enforce** - Preconditions, safety guards, and control modes are explicit in the API and state. - Tuning and config values are centralized and named by domain meaning. **Red flags** - Magic thresholds spread across commands. - Hidden mode switching via mutable globals. - "Do everything" methods whose behavior is controlled by call-order side effects. **Evidence of correctness** - APIs express intent (`setGoal`, `requestGoal`, `atGoal`) and measurable state. - Constants and config are grouped by subsystem and purpose. ### P10 — Treat architecture regressions as functional regressions **Enforce** - Reviewers block seam violations *even when robot behavior appears unchanged.* **Red flags** - "Works on the robot" used as justification for bypassing abstractions. - "Will refactor later" attached to vendor leaks and cross-layer coupling. **Evidence of correctness** - The PR discussion covers seam impact and test/sim impact — not only on-field behavior. --- ## Agent-facing review workflow (deterministic pass) Run this exact sequence for **each subsystem touched by the PR.** 1. **Map touched seams.** Identify whether the PR changes the IO seam, state seam, coordination seam, or several at once. 2. **Check vendor confinement.** Search the changed files for vendor imports appearing above IO implementations. 3. **Check interface shape.** Confirm the IO contract still supports REAL + SIM with no vendor leakage. 4. **Check dependency direction.** Verify no new *upward* dependency from a subsystem to coordinator/state policy. 5. **Check the test path.** Confirm the code can still be instantiated in sim-backed tests. 6. **Check the sim path.** Confirm there is no SIM-only behavior fork that bypasses subsystem logic. 7. **Check the logging path.** Confirm inputs and key decisions remain observable. 8. **Classify findings.** Assign each an S0–S3 severity and explain the seam impact. > If any of steps 2–7 fails, mark it as an **architectural finding even if the runtime behavior is currently correct.** --- ## Human-facing quick checklist Use these six questions in a live review conversation. - Can I swap a motor vendor in this subsystem **without touching subsystem logic**? - Can I run this changed behavior in **SIM without robot hardware**? - Can I write a **deterministic test** for this behavior with the existing seams? - Can I understand **why a failure happened from logs alone**? - Is there **one obvious place** where cross-subsystem safety rules live? - Did this PR **reduce** coupling, or just **hide** it? If any answer is "no," investigate before approval. --- ## Non-negotiable anti-patterns (auto-fail) These fail the review automatically unless they are justified *and* remediated: 1. Vendor types imported above IO implementations. 2. Commands or coordinators writing actuator outputs directly. 3. A "Superstructure" with no real transition/guard model. 4. Subsystem logic that cannot execute with `IOSim`. 5. New mechanism code that skips the IO seam by design. 6. A logging strategy that cannot reconstruct what the code saw. Any temporary exception must ship with a **dated remediation plan in the PR.** --- ## Finding output format Report every finding in this exact shape so agent and human reviews stay aligned and comparable across PRs. - **Severity:** S0 / S1 / S2 / S3 - **Principle:** P# - **Location:** file and symbol - **What leaked or coupled:** one sentence - **Why it matters:** one sentence, tied to test / sim / replay / safety / future change - **Minimum fix:** the smallest structural correction that restores seam integrity # Appendix E — The minimal worked example https://frc-code-scout.jointheleague.org/appendices/minimal-worked-example/ **This is the whole book, once, in the smallest robot that still has all the layers.** Two wheels, one color sensor, one job: *drive forward until the sensor sees the red line, then stop.* That is enough to need a hardware abstraction layer, two subsystems, a planner, a configuration object, and commands — and every one of them presents the same four-channel faceplate from [ch. 25](../part-3/25-portable-component-model.md): > **`Config`** in once · **`Command_in`** each tick · **`State`** out each tick · **`Command_out`** > returned, never pushed — advanced by one pure `update(Command_in, Observations)`. [Ch. 25](../part-3/25-portable-component-model.md) worked the contract for a single elevator; [ch. 26](../part-3/26-portable-motor-interface.md)–[28](../part-3/28-robotstate-superstructure-blocks.md) worked it at one altitude at a time. This appendix stacks three altitudes into one running (well, readable) robot, so you can watch a command descend the tree and state climb back up. It is *illustrative of the contract, not a finished library* — real code carries more fields, more status, and the lifecycle. ## The cast, by fill-pattern The fill-pattern *is* the taxonomy ([ch. 25](../part-3/25-portable-component-model.md)): which channels a component populates tells you what it is. | Component | Layer | `Config` | `Command_in` | `State` | `Command_out` | |---|---|:--:|---|---|---| | `MotorIO` (×2) | HAL leaf | CAN id, inversion | `MotorCommand` (volts) | `MotorState` | — (it *is* the metal) | | `ColorSensorIO` | HAL leaf | I²C port | — | `ColorSensorState` (raw counts) | — | | `Drive` | subsystem | gearing, wheel radius, gains | `DriveCommand` (m/s) | `DriveState` | 2× `MotorCommand` | | `ColorSense` | subsystem | target color, tolerance | — *(a sensor)* | `ColorSenseState` (meaning) | — | | `Superstructure` | executive | seek speed | `Goal` (driver intent) | `PlannerState` (mode, ready) | 1× `DriveCommand` | Read the rows and the recursion from [ch. 28](../part-3/28-robotstate-superstructure-blocks.md) is visible: the planner and the drive are *the same kind of component* — all four channels filled — differing only in whether `Command_out` feeds subsystems or motors. `ColorSense` fills neither command channel: that emptiness is what makes it a sensor, not a controller. This robot is deliberately too small to need a `RobotState` — with one drivebase and one sensor there is nothing to fuse — but when vision or odometry consumers arrive, the estimator of [ch. 28](../part-3/28-robotstate-superstructure-blocks.md) drops in beside these without touching them. ```d2 direction: down DRV: "Driver — A button" SUP: "Superstructure (planner)\nupdate(Goal, obs) → DriveCommand" DR: "Drive (subsystem)\nupdate(DriveCommand, obs) → 2× MotorCommand" CS: "ColorSense (sensor)\nupdate(obs) → ColorSenseState" LM: "MotorIO left" RM: "MotorIO right" CIO: "ColorSensorIO" DRV -> SUP: "Goal (Command_in)" SUP -> DR: "DriveCommand (Command_out)" DR -> LM: "MotorCommand" DR -> RM: "MotorCommand" LM -> DR: "MotorState (observations)" { style.stroke-dash: 4 } RM -> DR: "MotorState (observations)" { style.stroke-dash: 4 } CIO -> CS: "raw color (observations)" { style.stroke-dash: 4 } CS -> SUP: "ColorSenseState (state up)" { style.stroke-dash: 4 } DR -> SUP: "DriveState (state up)" { style.stroke-dash: 4 } ``` On disk it is the quartet-per-subsystem layout of [ch. 3](../part-1/03-the-io-seam.md), plus one pure logic file per level: ```text frc/robot/ Constants.java // every Config POD, filled in ONE place RobotContainer.java // run-mode selection + button → goal bindings superstructure/ Superstructure.java // shell: WPILib subsystem, wiring layer PlannerLogic.java // pure: the guarded transition function Goal.java PlannerConfig.java PlannerState.java PlannerObs.java PlannerTick.java subsystems/ drive/ Drive.java // shell DriveLogic.java // pure DriveConfig.java DriveCommand.java DriveState.java DriveObs.java DriveTick.java colorsense/ ColorSense.java // shell ColorSenseLogic.java // pure ColorSenseConfig.java ColorSenseState.java ColorSenseObs.java hal/ MotorIO.java MotorCommand.java MotorState.java MotorIOSparkMax.java // the ONLY files that import a vendor SDK MotorIOSim.java ColorSensorIO.java ColorSensorState.java ColorSensorIORevV3.java ColorSensorIOSim.java ``` ## Layer 1 — the hardware abstraction layer (leaf IO) The leaf is where the model touches metal ([ch. 26](../part-3/26-portable-motor-interface.md)). Each device gets a POD pair — its `Command_in` and its `State`, the state-space `u`/`x` — and an `…IO` interface that is nothing but the impure shell around them. Units are SI throughout (rad, m, V, A). ```java // hal/MotorState.java — the leaf's State channel: what the motor is measurably doing (x) public record MotorState( double positionRad, double velocityRadS, double appliedVolts, double currentAmps, boolean connected) {} // hal/MotorCommand.java — the leaf's Command_in (u); this robot only ever needs voltage mode public record MotorCommand(double volts) { public static MotorCommand neutral() { return new MotorCommand(0.0); } } // hal/MotorIO.java — the downward edge of the leaf component: read samples in, apply pushes out public interface MotorIO { MotorState read(); // once per tick, into a POD void apply(MotorCommand cmd); // once per tick, out to metal } ``` A sensor leaf is the same shape with the command half *deleted* — no `apply`, because nothing commands a sensor: ```java // hal/ColorSensorState.java — raw device truth: normalized color counts + proximity public record ColorSensorState( double red, double green, double blue, int proximity, boolean connected) {} // hal/ColorSensorIO.java — a pure source: State out, nothing in public interface ColorSensorIO { ColorSensorState read(); } ``` The implementations are the only files in the robot allowed to import a vendor SDK, and they are named by device, not `…Real` ([ch. 3](../part-1/03-the-io-seam.md)) — the name documents the vendor at the seam: ```java // hal/MotorIOSparkMax.java — com.revrobotics lives HERE and nowhere above public class MotorIOSparkMax implements MotorIO { private final SparkMax motor; private final RelativeEncoder encoder; public MotorIOSparkMax(int canId, boolean inverted) { motor = new SparkMax(canId, MotorType.kBrushless); motor.configure(new SparkMaxConfig().inverted(inverted), ResetMode.kResetSafeParameters, PersistMode.kPersistParameters); encoder = motor.getEncoder(); } @Override public MotorState read() { return new MotorState( Units.rotationsToRadians(encoder.getPosition()), Units.rotationsPerMinuteToRadiansPerSecond(encoder.getVelocity()), motor.getAppliedOutput() * motor.getBusVoltage(), motor.getOutputCurrent(), !motor.getFaults().can); } @Override public void apply(MotorCommand cmd) { motor.setVoltage(cmd.volts()); } } // hal/ColorSensorIORevV3.java public class ColorSensorIORevV3 implements ColorSensorIO { private final ColorSensorV3 sensor = new ColorSensorV3(I2C.Port.kOnboard); @Override public ColorSensorState read() { var c = sensor.getColor(); return new ColorSensorState(c.red, c.green, c.blue, sensor.getProximity(), sensor.isConnected()); } } ``` And the sim implementations — WPILib only — are just another `…IO`, which is why the sim *is* the mock every test uses: ```java // hal/MotorIOSim.java — a physics model behind the same interface public class MotorIOSim implements MotorIO { private final DCMotorSim sim = new DCMotorSim( LinearSystemId.createDCMotorSystem(DCMotor.getNEO(1), 0.004, Constants.DRIVE.gearRatio()), DCMotor.getNEO(1)); private double volts = 0.0; @Override public MotorState read() { sim.setInputVoltage(volts); sim.update(0.020); return new MotorState(sim.getAngularPositionRad(), sim.getAngularVelocityRadPerSec(), volts, sim.getCurrentDrawAmps(), true); } @Override public void apply(MotorCommand cmd) { volts = cmd.volts(); } } // hal/ColorSensorIOSim.java — a settable fake: a test (or a sim script) decides what the robot sees public class ColorSensorIOSim implements ColorSensorIO { private ColorSensorState next = new ColorSensorState(0.25, 0.5, 0.25, 0, true); public void set(ColorSensorState s) { next = s; } @Override public ColorSensorState read() { return next; } } ``` ## Layer 2 — the subsystems A subsystem is two files that must never be confused: a **pure logic class** (the four PODs and the `update`) and a **thin impure shell** (a WPILib `Subsystem` whose `periodic()` does exactly `read → update → apply`). The logic computes; the shell touches the world. ### Drive — a controller over two motor leaves First the four channels plus the observations and the tick, each its own POD: ```java // subsystems/drive/DriveConfig.java — identity + calibration; set once (§Config, ch. 25) public record DriveConfig( int leftCanId, int rightCanId, double gearRatio, double wheelRadiusM, double kP, double kV, // ONE velocity loop, above the IO line (Decision A, ch. 3) double maxSpeedMps) {} // subsystems/drive/DriveCommand.java — Command_in: intent in field units. Volts never appear here. public record DriveCommand(double speedMps) { public static DriveCommand stop() { return new DriveCommand(0.0); } } // subsystems/drive/DriveState.java — State = estimate + status (ch. 25) public record DriveState( double distanceM, double speedMps, // estimate boolean atGoal, boolean connected) { // status public static DriveState initial() { return new DriveState(0, 0, false, false); } } // subsystems/drive/DriveObs.java — Observations: the children's State + the tick's time. // The timestamp is data, delivered like any other fact — no component ever reads a clock. public record DriveObs(double timestampS, MotorState left, MotorState right) {} // subsystems/drive/DriveTick.java — what update RETURNS: State′ and Command_out public record DriveTick(DriveState state, MotorCommand left, MotorCommand right) {} ``` Then the pure step. No hardware handle, no clock, no scheduler — feedforward plus proportional feedback, written once, shared by real and sim because the loop is above the IO line: ```java // subsystems/drive/DriveLogic.java public class DriveLogic { private final DriveConfig cfg; public DriveLogic(DriveConfig cfg) { this.cfg = cfg; } public DriveTick update(DriveCommand cmd, DriveObs obs) { double distanceM = (toMeters(obs.left().positionRad()) + toMeters(obs.right().positionRad())) / 2.0; double speedMps = (toMps(obs.left().velocityRadS()) + toMps(obs.right().velocityRadS())) / 2.0; double target = Math.max(-cfg.maxSpeedMps(), Math.min(cfg.maxSpeedMps(), cmd.speedMps())); double volts = cfg.kV() * target + cfg.kP() * (target - speedMps); var state = new DriveState(distanceM, speedMps, Math.abs(target - speedMps) < 0.05, obs.left().connected() && obs.right().connected()); return new DriveTick(state, // emission is the return value — new MotorCommand(volts), new MotorCommand(volts)); // update never touches a MotorIO } private double toMeters(double rad) { return rad / cfg.gearRatio() * cfg.wheelRadiusM(); } private double toMps(double radS) { return radS / cfg.gearRatio() * cfg.wheelRadiusM(); } } ``` And the shell — the only impure code, three lines of wiring plus logging: ```java // subsystems/drive/Drive.java public class Drive extends SubsystemBase { private final DriveLogic logic; private final MotorIO left, right; // vendor types live below this line private DriveCommand cmd = DriveCommand.stop(); private DriveState state = DriveState.initial(); public Drive(DriveConfig cfg, MotorIO left, MotorIO right) { this.logic = new DriveLogic(cfg); this.left = left; this.right = right; } public void accept(DriveCommand c) { cmd = c; } // Command_in — the wiring layer calls this public DriveState state() { return state; } // State — flows up to whoever observes it @Override public void periodic() { var obs = new DriveObs(Timer.getFPGATimestamp(), left.read(), right.read()); // 1. read var tick = logic.update(cmd, obs); // 2. pure step left.apply(tick.left()); // 3. actuate right.apply(tick.right()); state = tick.state(); // 4. log cmd, obs, tick — all PODs, so the log IS the replay input (ch. 29) } } ``` ### ColorSense — a sensor over one sensor leaf The sensor subsystem exists to convert *device truth* (raw color counts) into *robot meaning* ("that is the line we stop at"). Its command channels are empty — `update` takes only observations and returns only state — which per the fill-pattern table makes it a sensor, the same fill-pattern `RobotState` would have ([ch. 28](../part-3/28-robotstate-superstructure-blocks.md)): ```java // subsystems/colorsense/ColorSenseConfig.java — what color we hunt, and how sure we must be public record ColorSenseConfig( double targetRed, double targetGreen, double targetBlue, double matchTolerance) {} // subsystems/colorsense/ColorSenseState.java — meaning, not counts: estimate + status public record ColorSenseState(double matchError, boolean onTarget, boolean connected) { public static ColorSenseState initial() { return new ColorSenseState(1.0, false, false); } } // subsystems/colorsense/ColorSenseObs.java public record ColorSenseObs(double timestampS, ColorSensorState raw) {} // subsystems/colorsense/ColorSenseLogic.java — no Command_in, no Command_out: update(obs) only public class ColorSenseLogic { private final ColorSenseConfig cfg; public ColorSenseLogic(ColorSenseConfig cfg) { this.cfg = cfg; } public ColorSenseState update(ColorSenseObs obs) { double err = Math.abs(obs.raw().red() - cfg.targetRed()) + Math.abs(obs.raw().green() - cfg.targetGreen()) + Math.abs(obs.raw().blue() - cfg.targetBlue()); boolean onTarget = obs.raw().connected() && err < cfg.matchTolerance(); return new ColorSenseState(err, onTarget, obs.raw().connected()); } } // subsystems/colorsense/ColorSense.java — the shell; note there is nothing to apply public class ColorSense extends SubsystemBase { private final ColorSenseLogic logic; private final ColorSensorIO io; private ColorSenseState state = ColorSenseState.initial(); public ColorSense(ColorSenseConfig cfg, ColorSensorIO io) { this.logic = new ColorSenseLogic(cfg); this.io = io; } public ColorSenseState state() { return state; } @Override public void periodic() { state = logic.update(new ColorSenseObs(Timer.getFPGATimestamp(), io.read())); // log obs + state } } ``` ## Layer 3 — the superstructure (the planner) The executive presents the same four-channel faceplate one altitude up ([ch. 28](../part-3/28-robotstate-superstructure-blocks.md)): its `Command_in` is a robot-wide `Goal`, its `Observations` are its children's `State`, its `Command_out` is a per-subsystem command, and its `update` **is** the guarded transition function of [ch. 5](../part-1/05-the-coordination-seam.md) — every mode change and every safety rule in one function, so there is exactly one place to read when the robot does something surprising. ```java // superstructure/Goal.java — Command_in at the top of the tree: driver intent, nothing lower public enum Goal { IDLE, SEEK_LINE } // superstructure/PlannerConfig.java public record PlannerConfig(double seekSpeedMps) {} // superstructure/PlannerState.java — at this altitude status IS the primary output (ch. 25) public record PlannerState(Goal goal, Mode mode, boolean ready) { public enum Mode { IDLE, SEEKING, HOLDING } public static PlannerState initial() { return new PlannerState(Goal.IDLE, Mode.IDLE, false); } } // superstructure/PlannerObs.java — the planner sees CHILDREN'S State, never devices: no volts, no counts public record PlannerObs(double timestampS, DriveState drive, ColorSenseState color) {} // superstructure/PlannerTick.java — one outgoing edge: the drive. The sensor gets no command. public record PlannerTick(PlannerState state, DriveCommand driveCmd) {} ``` ```java // superstructure/PlannerLogic.java — the pure planner public class PlannerLogic { private final PlannerConfig cfg; private PlannerState.Mode mode = PlannerState.Mode.IDLE; // internal memory (ch. 25), not State public PlannerLogic(PlannerConfig cfg) { this.cfg = cfg; } public PlannerTick update(Goal goal, PlannerObs obs) { mode = switch (mode) { // ALL transitions pass through this switch case IDLE -> goal == Goal.SEEK_LINE ? PlannerState.Mode.SEEKING : PlannerState.Mode.IDLE; case SEEKING -> goal == Goal.IDLE ? PlannerState.Mode.IDLE : obs.color().onTarget() ? PlannerState.Mode.HOLDING // the sensor ends the seek : PlannerState.Mode.SEEKING; case HOLDING -> goal == Goal.SEEK_LINE ? PlannerState.Mode.HOLDING : PlannerState.Mode.IDLE; }; // The interlock, in the one legal place: never drive blind. If the sensor reports // unhealthy, the seek is vetoed regardless of what the driver requested. boolean safeToDrive = obs.color().connected(); var driveCmd = (mode == PlannerState.Mode.SEEKING && safeToDrive) ? new DriveCommand(cfg.seekSpeedMps()) : DriveCommand.stop(); return new PlannerTick( new PlannerState(goal, mode, mode == PlannerState.Mode.HOLDING), driveCmd); // returned, never pushed } } ``` The planner's shell is where the two coupling rules meet without contradiction: the *logic* holds no reference to any child (it returns its commands), while the *shell* — which is part of the outer wiring layer — routes `Command_out` to the child's `Command_in`: ```java // superstructure/Superstructure.java public class Superstructure extends SubsystemBase { private final PlannerLogic logic; private final Drive drive; // shells, never logic — the pure planner can't see these private final ColorSense colorSense; private Goal goal = Goal.IDLE; private PlannerState state = PlannerState.initial(); public Superstructure(PlannerConfig cfg, Drive drive, ColorSense colorSense) { this.logic = new PlannerLogic(cfg); this.drive = drive; this.colorSense = colorSense; } // Intent enters the tree as a WPILib Command — a button binds to a GOAL, never to a motor. public Command requestGoal(Goal g) { return runOnce(() -> goal = g); } public PlannerState state() { return state; } @Override public void periodic() { var obs = new PlannerObs(Timer.getFPGATimestamp(), drive.state(), colorSense.state()); // state up: children's last-published State var tick = logic.update(goal, obs); drive.accept(tick.driveCmd()); // command down: Command_out → child Command_in state = tick.state(); // log goal, obs, tick } } ``` One honest scheduling note: `drive.state()` is the state the drive published on its own last `periodic()`, so state climbs the tree with one tick of transport delay — exactly as signals settle through a block diagram. It is deterministic, it is logged, and at 50 Hz it is invisible; do not "fix" it by reaching inside the child. ## The configuration object Every `Config` POD is *defined* next to its component and *filled* in exactly one file. Numbers live here and nowhere else — a CAN id in a subsystem file is a smell: ```java // Constants.java public final class Constants { public enum Mode { REAL, SIM } public static final Mode MODE = RobotBase.isReal() ? Mode.REAL : Mode.SIM; public static final DriveConfig DRIVE = new DriveConfig( 1, 2, // CAN ids: left, right 8.46, // gear ratio 0.0762, // wheel radius, m (3 in) 0.5, 2.3, // kP (V per m/s of error), kV (V per m/s) 3.0); // max speed, m/s public static final ColorSenseConfig COLOR = new ColorSenseConfig( 0.55, 0.32, 0.13, // red field tape, normalized RGB 0.12); // match tolerance public static final PlannerConfig PLANNER = new PlannerConfig(1.0); // seek at 1 m/s } ``` The boundary test from [ch. 25](../part-3/25-portable-component-model.md) applies to every field: if it changes every loop it is a `Command`; if it identifies or calibrates the component across a session it is `Config`. Nothing in this file changes during a match. ## Commands and the wiring layer `RobotContainer` is the outer wiring layer: it owns the **selection point** (the only code that knows which implementation backs each seam, keyed off run mode — [ch. 3](../part-1/03-the-io-seam.md)) and the **bindings** (buttons carry intent; no binding ever names a motor or a voltage): ```java // RobotContainer.java public class RobotContainer { private final Drive drive; private final ColorSense colorSense; private final Superstructure superstructure; private final CommandXboxController controller = new CommandXboxController(0); public RobotContainer() { switch (Constants.MODE) { // the selection point case REAL -> { drive = new Drive(Constants.DRIVE, new MotorIOSparkMax(Constants.DRIVE.leftCanId(), false), new MotorIOSparkMax(Constants.DRIVE.rightCanId(), true)); colorSense = new ColorSense(Constants.COLOR, new ColorSensorIORevV3()); } case SIM -> { drive = new Drive(Constants.DRIVE, new MotorIOSim(), new MotorIOSim()); colorSense = new ColorSense(Constants.COLOR, new ColorSensorIOSim()); } } superstructure = new Superstructure(Constants.PLANNER, drive, colorSense); // Commands: the driver expresses INTENT. The planner owns execution. controller.a().onTrue(superstructure.requestGoal(Goal.SEEK_LINE)); controller.b().onTrue(superstructure.requestGoal(Goal.IDLE)); } // The same goal is an autonomous routine for free — intent is reusable (ch. 5): public Command autoSeekLine() { return superstructure.requestGoal(Goal.SEEK_LINE) .andThen(Commands.waitUntil(() -> superstructure.state().ready())); } } ``` Follow one press of the A button through the tree: the binding sets a `Goal` (intent); next tick the planner's pure `update` turns `SEEK_LINE` into a `DriveCommand` of 1.0 m/s (plan); the drive's pure `update` turns 1.0 m/s into ~2.8 V of feedforward-plus-feedback (control); the `MotorIO` turns volts into a vendor call (metal). Meanwhile raw color counts climb the other way, becoming `onTarget = true` at the sensor and a `SEEKING → HOLDING` transition at the planner, which zeroes the `DriveCommand` on the way back down. Four layers, and no layer speaks a vocabulary that belongs to another. ## The payoff — testing the plan with PODs Because every `update` is a pure function of PODs, the interesting behavior of this robot — *does it actually stop on the line? does it refuse to drive with a dead sensor?* — is testable with no scheduler, no HAL bootstrap, and no hardware. Construct observations by hand and assert on the tick: ```java class PlannerLogicTest { private PlannerObs obs(double t, ColorSenseState color) { return new PlannerObs(t, new DriveState(0, 0, false, true), color); } private static final ColorSenseState OFF_LINE = new ColorSenseState(0.9, false, true); private static final ColorSenseState ON_LINE = new ColorSenseState(0.05, true, true); private static final ColorSenseState DEAD = new ColorSenseState(1.0, false, false); @Test void seekDrivesUntilLineThenHolds() { var logic = new PlannerLogic(Constants.PLANNER); var seeking = logic.update(Goal.SEEK_LINE, obs(0.00, OFF_LINE)); assertEquals(1.0, seeking.driveCmd().speedMps()); // seeking: commanded at seek speed var holding = logic.update(Goal.SEEK_LINE, obs(0.02, ON_LINE)); assertEquals(0.0, holding.driveCmd().speedMps()); // line seen: the plan halts the drive assertTrue(holding.state().ready()); } @Test void deadSensorVetoesTheSeek() { var logic = new PlannerLogic(Constants.PLANNER); var tick = logic.update(Goal.SEEK_LINE, obs(0.00, DEAD)); assertEquals(0.0, tick.driveCmd().speedMps()); // the interlock, provably enforced } } ``` The same trick works one layer down (`DriveLogic`: feed a `DriveObs`, assert on volts) and one layer up in sim (bind the `ColorSensorIOSim`, run the whole robot, script the line appearing). And because the shells log exactly the PODs the logic consumes and returns, the match log *is* a replay input ([ch. 29](../part-3/29-telemetry-replay-tests.md)) — the same `update` functions re-run over recorded `Command_in` + `Observations` reproduce the match decision-for-decision. ## Checklist — what the example just demonstrated - **Four channels at every altitude** — `Config` / `Command_in` / `State` / `Command_out`, as PODs, for the motor, the sensor, both subsystems, and the planner; the fill-pattern classified each. - **One pure `update` per component** — emission as the return value, never `child.set…` from inside the logic; the shells (and only the shells) touch hardware and route commands. - **No clock reads inside `update`** — every timestamp arrived inside an `Obs` POD. - **Vendor confinement** — `com.revrobotics` appears in `MotorIOSparkMax` and `ColorSensorIORevV3` and nowhere else; swap to TalonFX by writing one new leaf file. - **Config is a channel, filled once** — every number in `Constants`, defined beside its component. - **Commands are the edges** — a button binds to a `Goal`; the planner's `Command_out` is the drive's `Command_in`; the drive's `Command_out` is the motors'. No layer skips a level. - **The interlock lives in one function** — the dead-sensor veto is in `PlannerLogic.update` and is unit-tested in four lines. If you can find these seven properties in your own robot — whatever the mechanisms — the architecture is intact. # Lessons from Outside https://frc-code-scout.jointheleague.org/appendices/lessons-from-outside/ # Lessons from Outside *What the broader robotics world treats as table stakes — and FRC mostly skips.* The Elite Architecture was reconstructed from inside FRC. This closing section steps outside it. ROS, Nav2, MoveIt, and the autonomous-driving stack converged on disciplines that FRC code rarely adopts — graceful degradation, managed lifecycle, process isolation, spec-driven generation — and seeing them named is the clearest way to see what the architecture is still missing. It begins with a survey chapter and grows one lesson at a time: each deserves its own treatment, and those are added here as they are written. 1. [Lessons from outside FRC](01-lessons-from-outside.md) — the outside-in view, and what it sets up. 2. [Spec-in, code-out — generators against the seam](02-code-generators.md) — RobotBuilder, Tuner X, YAGSL, and the LLM generators scored against the IO seam, and the one rule: generate the constants, own the architecture. # Lessons from outside FRC https://frc-code-scout.jointheleague.org/appendices/lessons-from-outside/01-lessons-from-outside/ The Elite Architecture was reconstructed from FRC code, but FRC is a small corner of robotics. Looking outward shows what the rest of the field treats as non-negotiable — and which of those disciplines this architecture still skips. The thesis: **FRC independently reinvented the structural core but is missing most of the runtime and process disciplines.** The seams are spatial (where code lives); the gaps are temporal (what happens over time, across crashes, across robots, across seasons). ## Where FRC already lands The broader field splits "robot architecture" into two questions. *How do the pieces talk?* — a component model, dominantly a graph of components exchanging typed messages (ROS 2, OROCOS, the BRICS "5 Cs": Computation, Communication, Coordination, Configuration, Composition). *How does the robot decide?* — a 40-year arc from Sense-Plan-Act through subsumption to the **three-layer hybrid**: a fast reactive control layer, a slow deliberative planning layer, and an executive that sequences between them. Modern self-driving stacks are this made concrete (Localization → Perception → Planning → Control). In that vocabulary, FRC is a single-process, synchronous, 20 ms system on a known map. The IO seam is **ports-and-adapters** — exactly what `ros2_control` enforces. `RobotState` is the **observer** half of control theory's plant–observer split. The `Superstructure` is the **executive layer**. The whole robot is a degenerate three-layer hybrid with the deliberative layer mostly empty. Two things the broader field uses that FRC correctly does *not* need, so this stays engineering rather than mimicry: a multi-process DDS message bus is pure overhead for one RIO plus one coprocessor (in-process typed interfaces get the decoupling without the transport), and SLAM is unnecessary because the field ships as CAD months ahead — FRC localization is a known-map problem, which is why AprilTag pose fusion, not SLAM, is the baseline. ## The seven importable disciplines | # | Lesson | Outside source | FRC status | Rubric tie | |---|---|---|---|---| | 1 | Record-and-replay as a debugging *culture* | `rosbag` (universal) | seam built, dividend uncollected (~1 team) | D5 | | 2 | Reactive decision-making as the top-level brain | Nav2 behavior-tree navigator | fixed sequences (BT ~1 team) | D2/D6 | | 3 | Coordination as a *planning* problem | MoveIt / OMPL, A\* | hand-coded interlocks (254 alone) | D2 | | 4 | Sim-first development + ground-truth testing | Isaac Sim, Gazebo, CARLA | sim mostly echoes setpoints | D3/D4 | | 5 | Lifecycle + graceful degradation | ROS 2 managed nodes | **no FRC analog** (~2 teams) | D5 | | 6 | Real-time budgeting as an explicit constraint | OROCOS, WCET | one loop, blocked freely | D1/D3 | | 7 | Shared, *versioned* interface standards | `ros2_control` | copy-paste between teams | D8 | Three of these are the highest-leverage gaps. **Replay culture (1)** is the cheapest win — the seam is already built; the field treats the match log the way ROS treats a bag, the first artifact you reach for, and robot time is *scarcer* for a school team, which makes replay more valuable in FRC, not less. **Reactive autonomy (2)** is the biggest competitive unlock — FRC autos are almost universally fixed sequences, and the real world does not honor a script; the field's answer for decades has been to decide intent every cycle and let the executive carry it out safely. **Lifecycle and graceful degradation (5)** is the largest true blind spot with no FRC vocabulary at all: a robot that loses a camera or browns out a controller mid-match should degrade predictably, not into undefined behavior — and the structural hook (a null-object IO that reports stale rather than crashing) is one the architecture already has but rarely uses. ## The generators reward the opposite axis A whole class of FRC tools takes a spec and hands you robot code — RobotBuilder, CTRE Tuner X's swerve generator, YAGSL, and the not-yet-real LLM generators. They share one property: **every one optimizes time-to-driving-robot, which is the opposite axis from the rubric.** The rubric rewards swappability, sim-testability, and vendor decoupling; the generators reward minutes-to-first-drive. Two of them push vendor types *above* the seam (Tuner X makes `com.ctre` the drivetrain) or hide the seam inside a black box you can't trace (YAGSL). The reconciling move is the practical takeaway and the one elite teams already make: **generate the constants, own the architecture.** Ingest the generator's tuned numbers — the swerve offsets, the characterization gains — but keep them behind your own IO seam as files you own, so the vendor stops at the line. This is the [foundation-first](../how-we-developed-this/05-foundation-first.md) discipline applied to the tools themselves. These gaps — degradation, lifecycle, a versioned interface standard, one shape that spans devices and executives — are exactly what [Part III](../../part-3/) sets out to close. The corpus also holds sound patterns that aren't the default and don't fit Part III's single model; [Part I ch. 8](../../part-1/08-alternatives.md) catalogs them. # 2. Spec-in, code-out — generators against the seam https://frc-code-scout.jointheleague.org/appendices/lessons-from-outside/02-code-generators/ **A whole class of FRC tools takes a specification and hands you back robot code — and every one of them optimizes the wrong axis for this book.** [Lesson 1](01-lessons-from-outside.md) named the pattern in a paragraph and left a rule hanging: *generate the constants, own the architecture.* This chapter is the full treatment. It walks the generator landscape tool by tool, shows why time-to-driving-robot and seam quality are close to orthogonal, and turns that rule from a slogan into a build recipe. It does **not** cover path and trajectory generators (PathPlanner, Choreo, the retired PathWeaver) — those emit *motion*, not robot code, and belong with the autonomy material. ## The one axis that sorts the landscape Every tool below answers two questions, and the answers correlate. *Who owns the drivetrain code after generation* — you, or a black-box library you depend on? And *where do the vendor SDK types live* — below [the IO seam](../../part-1/03-the-io-seam.md) you control, smeared across your subsystems, or hidden inside the library? The rubric's golden rule is that `com.ctre.*` / `com.revrobotics.*` / `org.photonvision.*` must **not** appear above the seam, and that subsystems must be unit-testable in sim. Measured against that, the generators land like this: | Tool | Spec format | Round-trip model | Vendor coupling | You own the code? | Rubric ceiling | |---|---|---|---|---|---| | **WPILib RobotBuilder** | GUI subsystem/command tree | Protected regions — true round-trip | Concrete HW classes *in* subsystems; no seam | Yes (skeleton) | Low — **deprecated, removed 2027** | | **CTRE Phoenix Tuner X swerve generator** | Wizard + characterization | One-shot, no markers | **Maximal** — `com.ctre.phoenix6` *is* the drivetrain; all-CTRE HW | Yes (emitted file) | High *performance*, zero seam — locked to CTRE | | **YAGSL** | JSON config directory | Black-box library (vendordep) | Hidden inside the lib; version-coupled | No — only a thin wrapper | Mid — can't trace/extend, replay hard | | **AI / LLM** | Natural-language prompt | N/A | Whatever it hallucinates — often deprecated APIs | Yes | Low — no traction as of 2026 | ## RobotBuilder — the original, now dying RobotBuilder is the WPILib GUI tool: you decompose the robot into a tree of subsystems and commands, set ports and PID gains, bind commands to buttons, and it emits a Java/C++/Python **skeleton** — declarations, button mappings, default-command wiring — that you finish by hand. Its round-trip model is genuinely the most sophisticated of anything here: generated code lives between `// BEGIN AUTOGENERATED CODE` / `// END AUTOGENERATED CODE` markers, regenerates *inside* them, and preserves your edits *outside* them. The architecture it produces is exactly the anti-pattern this book exists to flag. Generated subsystems reference concrete hardware classes directly — `PWMVictorSPX`, `AnalogPotentiometer` — and the generated `PIDSubsystem` reads sensors and drives motors *inside* the subsystem. There is no swappable IO layer, nothing isolated for a sim test; the subsystem owns its hardware. The leaking types are mostly WPILib framework classes rather than `com.ctre.*`, but the structure is the one the seam replaces. And the headline fact settles the argument: RobotBuilder is **officially deprecated and scheduled for removal in 2027** due to declining usage. The official toolchain is retiring the GUI-skeleton approach outright — a strong signal about where the idea leads. ## Tuner X — the one with real traction, and maximum coupling CTRE's Phoenix Tuner X swerve project generator is the tool teams actually reach for, and it is genuinely good at what it does. You set device IDs and characterize the modules inside a wizard — encoder inverts, drivetrain inverts, a verification routine — then click generate and get a complete FRC project: a `TunerConstants.java` full of measured device IDs, CANcoder offsets, slip-current limits, and Slot0 gains, plus a `CommandSwerveDrivetrain` and a `createDrivetrain()` factory. It ships built-in simulation, uses FOC automatically, and produces a drivable robot in a few hours — ten minutes if firmware and IDs are already set. Community consensus favors it for all-CTRE teams, and few teams can sustainably maintain a hand-rolled equivalent during build season. Even YAGSL recommends it for all-CTRE hardware and warns you when you run YAGSL on an all-CTRE config. The cost is total, by construction. The generated subsystem extends `com.ctre.phoenix6.swerve.SwerveDrivetrain`, parameterized on `TalonFX`, `CANcoder`, and `Pigeon2`, and the parent class constructs the hardware devices itself. It imports `com.ctre.phoenix6.{configs,hardware,signals,swerve}` extensively; it is built on Phoenix's own proprietary swerve API, not WPILib's template. There is no seam — the entire drivetrain *is* the vendor SDK, sitting above any IO line, requiring an all-CTRE stack: eight Talon FX motors, CTRE steer feedback, a Pigeon 2. You cannot swap in a REV motor without abandoning the generated architecture. The generator buys a high *performance* ceiling and a low *time* floor at the exact cost of the D1 seam; a team that stops at the emitted output scores well on "does it drive" and poorly on "could you swap the vendor, replay a match, or unit-test the module." (REV's MAXSwerve template does the same for the REV stack; in the corpus it surfaced mainly as a *config preset* bundled inside YAGSL, not a standalone code emitter.) ## YAGSL — a seam you don't own YAGSL inverts the model: you don't get emitted source at all. You author a JSON spec under `src/main/deploy/swerve/` — `swervedrive.json`, per-corner module files, physical and PID properties — and a single `SwerveParser(dir).createSwerveDrive(...)` call instantiates the entire drivetrain **at runtime**. You write and own only a thin `SwerveSubsystem` wrapper and your commands; the drivetrain implementation lives inside the library. Its real advantage over CTRE's generator is multi-vendor reach: motor controllers are selected by a JSON `type` string across three vendor families — REV, CTRE, and Thrifty Nova — abstracted behind an internal `SwerveMotor` wrapper, with a Flutter desktop configurator to build and validate the configs. That wrapper is where the subtlety lives. YAGSL *does* abstract vendors, and the author frames the `SwerveMotor` wrapper as "the IO layer"; it ships sim objects, maple-sim support, and AdvantageScope-compatible logging. But it is the **library's** seam, not yours. You cannot code-trace or extend the per-calculation behavior, which is precisely why experienced teams decline it — *"you lose control over how the swerve calculates,"* which hurts both teaching and on-field diagnosis. Concretely: **AdvantageKit replay is famously hard on YAGSL** — AK requires modifying user code, which conflicts with YAGSL's self-contained design, so bolting on [record-and-replay](../../part-1/07-cross-cutting-practices.md) means forking the library; it uses plain `VelocityVoltage` with no FOC; and it is version-coupled, a pinned vendordep whose configs break across upgrades so you migrate the *JSON*, not your code. YAGSL wins where it is designed to: it produces a functioning swerve with little prior knowledge, which makes it the right call for a team with no returning programmers, and broad adoption means easy troubleshooting. The honest forum framing is that swerve libraries differ little in lap time because none model the hard dynamics — so the choice is about **ownership and learning**, not performance. ## AI and LLM generators — no prompt-to-robot tool, yet The skeptical verdict is clear: **as of the 2026 season there is no established prompt-to-robot-code generator with real adoption in FRC.** The structural reason is that vendor APIs change every year and the old training data dwarfs the new, so LLMs emit outdated code. Asked for a tank drive, GPT-5-mini produced a hybrid using `MotorControllerGroup` — deprecated since 2024 — and didn't actually use commands despite extending `SubsystemBase`; prompts for "PathPlanner + AdvantageKit + CTRE swerve" did not yield working code. What teams actually use AI for, per a 112-voter poll, is debugging and boilerplate subsystems and constants; only a handful used it for anything like swerve kinematics, and a substantial fraction reported an explicit "No AI" policy. It is enhanced autocomplete for repetitive IO classes, not an architecture generator — and it only accelerates people who already read and understand the output. The bounding datapoints run from a vibe-coded driver-station component that failed at its first competition and got the team's AI banned, to one frontier contributor reporting 100% AI-generated-or-reviewed code on a 2026 competition robot. The implication points straight back at this project. An LLM that actually understood the seam, the inputs-struct logging contract, and sim-backed tests is the unoccupied territory — the generators above produce vendor-coupled or hidden-seam code *fast*, and an architecture-aware generator is the gap. That is the thesis behind the `scaffold-robot` and `add-subsystem` skills: generate the **seam**, not just the wiring. ## The reconciling move — generate the numbers, own the seam **Every generator here optimizes time-to-driving-robot, and that axis is close to orthogonal to seam quality** — for CTRE's generator and YAGSL, actively opposed. The CTRE generator puts vendor types *above* any seam; YAGSL hides the seam in a versioned black box; RobotBuilder's subsystems own their hardware and the tool is being retired. None of them, used as shipped, lands a team above the middle of the modularity ladder — not because the code is bad, but because the seam the rubric rewards isn't there. The move that reconciles the two axes is to separate the two artifacts a generator conflates: the **characterized constants** and the **architecture**. The genuinely valuable output of the Tuner X wizard is `TunerConstants.java` — real, measured device IDs, CANcoder offsets, slip current, and Slot0 gains you'd otherwise hand-tune. Take those numbers; leave the vendor drivetrain behind. Ingest them behind a `ModuleIO` / `GyroIO` seam you own and can replay, exactly the seam [the drivetrain subsystem](../../part-2/19-the-drivetrain-subsystem.md) specifies. The build-spec line is one sentence: **let a generator produce the numbers; never let it produce the architecture.** That single rule turns every tool in this chapter from an architectural dead-end into a legitimate accelerator, and it is the [foundation-first](../how-we-developed-this/05-foundation-first.md) discipline applied to the tools themselves. Which tool a team should touch follows from where it sits. An **all-CTRE team** may use Tuner X — its output is fast and correct on CTRE hardware — but must wrap the constants behind its own seam rather than shipping the generated drivetrain. A **multi-vendor team without strong learning goals** may take YAGSL, accepting the hidden seam as the price of a working drive with little prior knowledge. A team with **elite ambition hand-writes the seam** and lets the generator supply only the tuned numbers. One frontier note keeps this from being settled. 6328's AdvantageKit swerve template already **ingests the Tuner X config directly** — it loads the generated constants but wraps them in a `ModuleIO` / `GyroIO` seam you own, can replay, and can extend (grafting 254's setpoint generator, for instance). For a team already on PathPlanner, AdvantageKit IO, and PhotonVision localization, experienced programmers recommend *this* over switching to YAGSL, because the optimizations YAGSL hides are present in the AK template in a form you can code-trace. It is the clearest existing proof that "generate the constants, own the architecture" is not a compromise but the strictly better path — and the strongest candidate to promote into `scaffold-robot` as a concrete generator-to-seam adapter.