Table of Contents
- Overview
- The Diagnostic Problem
- Hardware Inventory Pattern
- Lebot2 Hardware Inventory
- Dependency Hierarchy
- Layer 0: Hardware Components
- Layer 1: Subsystem Dependencies
- Layer 2: Behavior Dependencies
- Layer 3: Robot-Level Coordination
- Layer 4: Missions (Composite Autonomous Behaviors)
- Failure Impact Analysis
- POST Test Structure
- Level 0: Hardware Existence
- Level 1: Hardware Function
- Level 2: Subsystem Function
- Level 3: Integration Tests
- Level 4: Mission Tests
- Diagnostic OpMode Design
- Current Implementation
- Safety Features
- Competition Checklist
Overview
This document describes Iron Reign’s approach to systematic robot diagnostics. The goal is to quickly identify hardware failures before they manifest as confusing behavior-level bugs.
The core insight: When a robot misbehaves, the failure could be at any level:
- A motor burned out (hardware)
- A sensor cable came loose (hardware)
- A state machine has a logic error (behavior)
- Two subsystems are conflicting over a shared resource (coordination)
Without systematic diagnostics, debugging becomes guesswork. A structured POST (Power-On Self-Test) approach lets us verify each layer before assuming the problem is in the code.
The Diagnostic Problem
The Scenario
Your robot was working yesterday. Today, during autonomous, it drives forward but then spins in circles instead of making a clean turn toward the goal.
Possible causes:
- Right drive motor failed
- Left drive motor running at wrong power
- Pinpoint odometry cable loose (heading data invalid)
- PID turn gains are wrong (software)
- Vision isn’t detecting the AprilTag (wrong pipeline?)
- Battery voltage dropped, affecting motor performance
Without diagnostics, you might spend 30 minutes debugging your PID code when the actual problem is a loose I2C cable.
The Solution: Layered Verification
Test from the bottom up:
- Hardware exists - Can we talk to the device at all?
- Hardware functions - Does it respond to direct commands?
- Subsystem functions - Does the subsystem behavior work in isolation?
- Integration functions - Do coordinated behaviors work together?
- Mission functions - Do complete autonomous sequences work?
If Layer 1 fails, don’t bother debugging Layer 3.
Two Initialization Approaches
The diagnostic framework uses two different initialization strategies depending on what’s being tested:
| Levels | Initialization | Why |
|---|---|---|
| L0-L1 | Direct hardwareMap.get() |
Isolate hardware from subsystem code - if subsystem is broken, can still verify hardware works |
| L2+ | Full Robot initialization |
Test the same code path as competition - no maintenance drift |
Direct hardware (L0-L1):
// Tests FTC SDK level - proves hardware works regardless of subsystem code
leftRear = hardwareMap.get(DcMotorEx.class, "leftRear");
leftRear.setPower(0.3); // Direct SDK call
Robot initialization (L2+):
// Tests real code path - same as competition OpMode
robot = new Robot(hardwareMap, telemetry);
robot.intake.setBehavior(Intake.Behavior.INTAKE);
robot.update(); // Three-phase: readSensors → calc → act
Why this split matters:
- L0-L1 isolation: If someone breaks Loader.java, you can still verify the conveyor motor works
- L2+ integration: Tests use the same hardware names, directions, and configurations as real code
- No maintenance drift: If someone renames “conveyor” to “belt” in Loader.java, L2+ tests automatically follow (via Robot init)
Hardware Inventory Pattern
Before you can test hardware, you need to know what hardware exists. Every robot should have a documented hardware inventory.
What to Document
For each hardware component:
| Field | Description |
|---|---|
| Config Name | String used in hardwareMap.get() |
| Type | Java class (DcMotorEx, Servo, DistanceSensor, etc.) |
| I2C? | Yes/No - I2C devices need special attention |
| Wrapper | LazyMotor, CachedDistanceSensor, or None |
| Owner | Which subsystem owns this hardware |
| Notes | Direction, zero power behavior, special config |
Lebot2 Hardware Inventory
Reference Robot: lebot2 (2025-2026 DECODE season)
Drive System
| Config Name | Type | I2C? | Wrapper | Owner |
|---|---|---|---|---|
leftRear |
DcMotorEx | No | None (RR direct) | TankDrivePinpoint |
rightRear |
DcMotorEx | No | None (RR direct) | TankDrivePinpoint |
pinpoint |
GoBildaPinpointDriver | Yes | PinpointLocalizer | TankDrivePinpoint |
Notes:
- Drive motors bypass LazyMotor because RoadRunner needs direct control
leftReardirection is REVERSE so positive power = forward- Pinpoint provides heading (no separate IMU)
Ball Handling
| Config Name | Type | I2C? | Wrapper | Owner |
|---|---|---|---|---|
intake |
DcMotorEx | No | LazyMotor | Intake |
conveyor |
DcMotorEx | No | LazyMotor | Loader |
frontDist |
DistanceSensor | Yes | CachedDistanceSensor | Loader |
backDist |
DistanceSensor | Yes | CachedDistanceSensor | Loader |
Notes:
conveyordirection is REVERSE so negative power = balls toward rearconveyoris a shared resource (Intake requests, Launcher claims)- Distance sensors are Rev 2m sensors, threshold ~10cm for ball detection
Launch System
| Config Name | Type | I2C? | Wrapper | Owner |
|---|---|---|---|---|
shooter |
DcMotorEx | No | None | Launcher |
shooter2 |
DcMotorEx | No | None | Launcher |
paddle |
Servo | No | LazyServo | Launcher |
Notes:
shooteruses RUN_USING_ENCODER for velocity PID (setVelocity in deg/sec)shooterdirection is REVERSEshooter2is a slave motor (follower) to double flywheel power - planned additionpaddlepositions: DOWN=0.167, UP=0.447
Vision
| Config Name | Type | I2C? | Wrapper | Owner |
|---|---|---|---|---|
limelight |
Limelight3A | No (USB) | None | Vision |
Notes:
- Pipeline 0 = Blue alliance, Pipeline 1 = Red alliance
- Uses botpose for field localization and distance calculation
- USB device, not subject to I2C timing constraints
System
| Config Name | Type | Notes |
|---|---|---|
| (voltage sensor) | VoltageSensor | First available, used for battery monitoring |
| (Lynx modules) | LynxModule | Control hubs, used for bulk caching |
Hardware Summary
| Category | Count | Components |
|---|---|---|
| Motors | 5 (6 planned) | leftRear, rightRear, intake, conveyor, shooter (+shooter2) |
| Servos | 1 | paddle |
| Distance Sensors (I2C) | 2 | frontDist, backDist |
| Odometry (I2C) | 1 | pinpoint |
| Cameras (USB) | 1 | limelight |
Dependency Hierarchy
Layer 0: Hardware Components
This is the physical inventory above. Each component can be tested in isolation.
Virtual Sensors
Virtual sensors are computed values derived from one or more physical sensors. They abstract away implementation details from behavior code, allowing the underlying sensor strategy to change without modifying behaviors.
Update Order Matters:
Physical Sensors (Phase 1)
↓
Virtual Sensors (early Phase 2)
↓
Behaviors use virtual sensor values (Phase 2)
↓
Hardware writes (Phase 3)
Virtual sensors must be updated AFTER all physical sensors they depend on have been refreshed.
Current Virtual Sensors
| Virtual Sensor | Depends On | Provides | Used By |
|---|---|---|---|
Pose2d (Pinpoint) |
pinpoint (I2C) | x, y, heading | TankDrivePinpoint, Vision |
Loader.isFull() |
frontDist, backDist, ballCount | boolean | Intake, Robot |
Loader.isBallAtBack() |
backDist | boolean | Launcher |
Vision.getDistanceToGoal() |
limelight (botpose), MatchConfig | meters | Launcher |
Virtual Sensor Design Pattern
public class Loader implements Subsystem {
// Physical sensor wrappers
private CachedDistanceSensor frontSensor;
private CachedDistanceSensor backSensor;
// Virtual sensor state (computed in calc())
private boolean isFull = false;
@Override
public void readSensors() {
// Phase 1: Physical sensor reads only
frontSensor.refresh();
backSensor.refresh();
}
@Override
public void calc(Canvas fieldOverlay) {
// Phase 2a: Update virtual sensors first
updateVirtualSensors();
// Phase 2b: Behavior logic uses virtual sensor values
if (isFull()) {
// Stop intake
}
}
private void updateVirtualSensors() {
// Current implementation: count-based
isFull = (ballCount >= MAX_BALLS);
// Future: could use time-based confirmation
// isFull = bothSensorsSeeingBallFor(MIN_CONFIRM_TIME);
// Future: could use 3rd sensor
// isFull = frontBall && middleBall && backBall;
}
public boolean isFull() {
return isFull;
}
}
Benefits of Virtual Sensors
- Implementation hiding: Behaviors don’t know if
isFulluses counting, timing, or extra sensors - Easy upgrades: Add a 3rd ball sensor? Only change
updateVirtualSensors() - Testability: Can inject test values into virtual sensor state
- Clear update ordering: Virtual sensors always computed before behavior logic
Planned Virtual Sensor: Loader.isFullConfirmed()
Current isFull() is count-based (fast but can drift if counting misses events).
A more robust version could require time-based confirmation:
- Both frontDist and backDist see balls
- Belt has been running
- Condition held for N milliseconds
This guards against transient sensor noise or balls bouncing during loading.
Layer 1: Subsystem Dependencies
Each subsystem depends on specific hardware:
┌─────────────────────────────────────────────────────────────────────────┐
│ SUBSYSTEM │ HARDWARE DEPENDENCIES │
├────────────────────┼────────────────────────────────────────────────────┤
│ TankDrivePinpoint │ leftRear, rightRear, pinpoint │
│ │ (pinpoint provides: x, y, heading, velocities) │
├────────────────────┼────────────────────────────────────────────────────┤
│ Intake │ intake │
│ │ (also: conveyor via Loader.requestBeltForIntake) │
├────────────────────┼────────────────────────────────────────────────────┤
│ Loader │ conveyor, frontDist, backDist │
│ │ (arbitrates belt ownership) │
├────────────────────┼────────────────────────────────────────────────────┤
│ Launcher │ shooter, shooter2, paddle │
│ │ (also: conveyor via Loader.claimBeltForLauncher) │
│ │ (also: Vision for distance calculation) │
├────────────────────┼────────────────────────────────────────────────────┤
│ Vision │ limelight │
│ │ (provides: tx, ty, distance, hasBotPose) │
└─────────────────────────────────────────────────────────────────────────┘
Layer 2: Behavior Dependencies
Each behavior requires specific hardware to be functional:
┌─────────────────────────────────────────────────────────────────────────┐
│ BEHAVIOR │ REQUIRES │
├─────────────────────────────┼───────────────────────────────────────────┤
│ TankDrivePinpoint.MANUAL │ leftRear, rightRear │
│ TankDrivePinpoint.PID_TURN │ leftRear, rightRear, pinpoint (heading) │
│ TankDrivePinpoint.TRAJECTORY│ leftRear, rightRear, pinpoint (full pose) │
├─────────────────────────────┼───────────────────────────────────────────┤
│ Intake.OFF │ (none) │
│ Intake.INTAKE │ intake motor │
│ Intake.LOAD_ALL │ intake motor, conveyor (via Loader) │
│ │ frontDist, backDist (full detection) │
│ Intake.EJECT │ intake motor │
├─────────────────────────────┼───────────────────────────────────────────┤
│ Launcher.IDLE │ (none) │
│ Launcher.SPINNING │ shooter, shooter2, Vision (distance) │
│ Launcher.fire() │ shooter, shooter2, paddle, conveyor │
├─────────────────────────────┼───────────────────────────────────────────┤
│ Vision (always active) │ limelight │
└─────────────────────────────────────────────────────────────────────────┘
Layer 3: Robot-Level Coordination
Robot behaviors coordinate multiple subsystems:
┌─────────────────────────────────────────────────────────────────────────┐
│ ROBOT BEHAVIOR │ SUBSYSTEM BEHAVIORS COORDINATED │
├───────────────────┼─────────────────────────────────────────────────────┤
│ Robot.MANUAL │ All subsystems independent, driver controls each │
├───────────────────┼─────────────────────────────────────────────────────┤
│ Robot.ROLLTAKE │ Rolling Inake, intake system runs while robot drives│
│ │ to projected end of current ball group │
├───────────────────┼─────────────────────────────────────────────────────┤
│ Robot.SHIMYTAKE │ Inake balls while executing an forward / backward │
│ │ angling motion kind of like a W pattern, to gather │
│ │ balls in a loose group │
├───────────────────┼─────────────────────────────────────────────────────┤
│ Robot.TARGETING │ TankDrivePinpoint.PID_TURN (to vision tx) │
│ │ Vision (provides tx for turn target) │
├───────────────────┼─────────────────────────────────────────────────────┤
│ Robot.LAUNCH_ALL │ Launcher.SPINNING → fire() repeatedly │
│ │ Loader (belt claimed, ball feeding) │
│ │ Vision (continuous distance updates) │
│ │ Intake (suppressed during firing) │
└─────────────────────────────────────────────────────────────────────────┘
Layer 4: Missions (Composite Autonomous Behaviors)
Missions are high-level autonomous behaviors that compose lower-level behaviors into complete game objectives. Unlike Robot behaviors which coordinate subsystems for a single action, Missions sequence multiple actions with navigation and decision-making.
Mission Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ MISSION │ COMPOSES THESE BEHAVIORS │
├──────────────────────┼──────────────────────────────────────────────────┤
│ LaunchPreloads │ 1. Navigate from start to launch position │
│ │ 2. TARGETING (turn to goal AprilTag) │
│ │ 3. LAUNCH_ALL (fire all 3 preloaded balls) │
├──────────────────────┼──────────────────────────────────────────────────┤
│ BALLGroup │ 1. Navigate to ball group N location │
│ │ 2. INTAKE while driving through balls │
│ │ 3. Navigate back to launch position │
│ │ 4. TARGETING + LAUNCH_ALL │
│ │ 5. Increment N │
├──────────────────────┼──────────────────────────────────────────────────┤
│ OpenSesame │ 1. Navigate from current position to gate │
│ │ 2. Turn around if not facing gate │
│ │ 3. Back into gate to release the Classifier │
├──────────────────────┼──────────────────────────────────────────────────┤
│ Autonomous │ Primary mission that chains: │
│ (primary) │ LaunchPreloads → Do BallGroups (time permitting) │
└─────────────────────────────────────────────────────────────────────────┘
Mission Dependencies
Missions depend on helper functions and match configuration:
┌─────────────────────────────────────────────────────────────────────────┐
│ HELPER │ PURPOSE │
├──────────────────────┼──────────────────────────────────────────────────┤
│ MatchConfig │ Alliance (RED/BLUE), start position │
│ │ (AUDIENCE_WALL / GOAL_WALL) │
├──────────────────────┼──────────────────────────────────────────────────┤
│ FieldPositions │ Poses for all 4 starting positions │
│ │ Launch position(s), ball group locations │
│ │ Goal AprilTag IDs per alliance │
├──────────────────────┼──────────────────────────────────────────────────┤
│ NavigationHelper │ Compute intermediate waypoints │
│ │ Obstacle avoidance paths │
│ │ Alliance-specific mirroring │
└─────────────────────────────────────────────────────────────────────────┘
Starting Positions
| Position | Alliance | Field Location | Typical Strategy |
|---|---|---|---|
| RED_AUDIENCE | Red | Near audience, far from goals | Longer drive to launch |
| RED_GOAL | Red | Near goals | Quick preload launch |
| BLUE_AUDIENCE | Blue | Near audience, far from goals | Longer drive to launch |
| BLUE_GOAL | Blue | Near goals | Quick preload launch |
Mission Chaining and Success Handling
Missions can chain conditionally based on success/failure and available time:
public class Autonomous {
private int ballGroupIndex = 1; // Start with group 1
public void run() {
// Always attempt preloads first
boolean preloadsSuccess = launchPreloads.execute();
// Iterate through ball groups while time permits
while (preloadsSuccess && timer.seconds() < 25 && ballGroupIndex <= 3) {
boolean groupSuccess = ballGroup.execute(ballGroupIndex);
if (groupSuccess) {
ballGroupIndex++;
} else {
break; // Stop if a group fails
}
}
// End game: release classifier if time permits
if (timer.seconds() < 28) {
openSesame.execute();
}
}
}
Implementation Location
Missions can be implemented in:
- Autonomous.java - If tightly coupled to autonomous flow
- Missions.java (new class) - If missions should be reusable/testable independently
- Robot.java - If missions are simple enough to be Robot behaviors
Recommendation: Create a separate Missions.java class that:
- Takes Robot reference for subsystem access
- Provides individual mission methods
- Handles mission-level state machines
- Can be tested independently via diagnostics
Failure Impact Analysis
This matrix shows what breaks when each hardware component fails:
| If This Fails… | These Behaviors Break | Likely Symptoms |
|---|---|---|
leftRear |
ALL drive behaviors | Robot spins right or doesn’t move |
rightRear |
ALL drive behaviors | Robot spins left or doesn’t move |
pinpoint |
PID_TURN, TRAJECTORY | Turns overshoot wildly, trajectories drift |
intake |
INTAKE, LOAD_ALL, EJECT | Balls don’t enter robot |
conveyor |
LOAD_ALL, fire() | Balls don’t advance to launcher |
frontDist |
LOAD_ALL (partial) | Can’t detect incoming balls, may overfill |
backDist |
LOAD_ALL, fire() | Launch timing wrong, balls not detected |
shooter |
SPINNING, fire() | Balls dribble out weakly |
shooter2 |
SPINNING, fire() | Reduced launch power (still functional) |
paddle |
fire() | Balls can’t reach flywheel |
limelight |
TARGETING, distance calc | Can’t aim, launcher speed wrong |
Cascading Failures
Some failures cascade through the dependency hierarchy:
pinpoint fails
→ TankDrivePinpoint.PID_TURN fails
→ Robot.TARGETING fails
→ Can't aim at goal
→ Launch accuracy drops (even if Launcher is fine)
backDist fails
→ Loader can't detect ball ready
→ Launcher.fire() timing wrong
→ Robot.LAUNCH_ALL fires at wrong times
→ Balls miss or jam
This is why bottom-up testing matters. Fix Layer 0 problems before debugging Layer 3.
POST Test Structure
Level 0: Hardware Existence
Goal: Verify all devices are connected and responding.
| Test | Pass Condition | Failure Mode |
|---|---|---|
| Get leftRear from hardwareMap | No exception | “Device not found” |
| Get rightRear from hardwareMap | No exception | “Device not found” |
| Get pinpoint from hardwareMap | No exception | “Device not found” |
| Get intake from hardwareMap | No exception | “Device not found” |
| Get conveyor from hardwareMap | No exception | “Device not found” |
| Get frontDist from hardwareMap | No exception | “Device not found” |
| Get backDist from hardwareMap | No exception | “Device not found” |
| Get shooter from hardwareMap | No exception | “Device not found” |
| Get shooter2 from hardwareMap | No exception | “Device not found” (optional) |
| Get paddle from hardwareMap | No exception | “Device not found” |
| Get limelight from hardwareMap | No exception | “Device not found” |
| Voltage sensor reading | 10V < reading < 15V | Battery dead or disconnected |
Level 1: Hardware Function (Direct Control)
Goal: Verify each device responds to commands.
Motors
| Test | Procedure | Pass Condition |
|---|---|---|
| leftRear forward | setPower(0.3) for 0.5s | Encoder changes positively |
| leftRear reverse | setPower(-0.3) for 0.5s | Encoder changes negatively |
| rightRear forward | setPower(0.3) for 0.5s | Encoder changes positively |
| rightRear reverse | setPower(-0.3) for 0.5s | Encoder changes negatively |
| intake spin | setPower(0.5) for 0.5s | Motor moves (visual/sound) |
| conveyor spin | setPower(0.5) for 0.5s | Belt moves |
| shooter spin | setVelocity(500) for 1s | Velocity reaches target ±10% |
| shooter2 spin | setVelocity(500) for 1s | Velocity reaches target ±10% |
Servos
| Test | Procedure | Pass Condition |
|---|---|---|
| paddle to DOWN | setPosition(0.167), wait 0.5s | Visual confirmation |
| paddle to UP | setPosition(0.447), wait 0.5s | Visual confirmation |
Sensors
| Test | Procedure | Pass Condition |
|---|---|---|
| frontDist range | Read distance | 0 < reading < 8190mm |
| backDist range | Read distance | 0 < reading < 8190mm |
| frontDist detect | Place object at 5cm | Reading < 10cm |
| backDist detect | Place object at 5cm | Reading < 10cm |
| pinpoint heading | Rotate robot 90° | Heading changes ~90° |
| pinpoint position | Push robot 30cm | Position changes ~30cm |
| limelight connected | Check status | isConnected() == true |
| limelight pipeline | Set pipeline, read back | Pipeline matches |
Level 2: Subsystem Function (Behavior Tests)
Goal: Verify each subsystem’s behaviors work correctly.
TankDrivePinpoint
| Test | Procedure | Pass Condition |
|---|---|---|
| MANUAL forward | drive(0.3, 0, 0) for 1s | Robot moves forward |
| MANUAL turn | drive(0, 0, 0.3) for 1s | Robot rotates |
| PID_TURN | turnToHeading(90°) | Heading converges to 90° ±3° |
Intake
| Test | Procedure | Pass Condition |
|---|---|---|
| INTAKE behavior | setBehavior(INTAKE), wait 1s | Intake motor running |
| EJECT behavior | setBehavior(EJECT), wait 1s | Intake motor reversed |
Loader
| Test | Procedure | Pass Condition |
|---|---|---|
| Belt request | requestBeltForIntake() | Belt runs toward rear |
| Ball detection | Insert ball at front | frontDist triggers, ballCount increases |
| Ball ready | Move ball to back | backDist triggers |
Launcher
| Test | Procedure | Pass Condition |
|---|---|---|
| SPINNING behavior | setBehavior(SPINNING) | Flywheel reaches target velocity |
| Paddle control | (internal) fire() | Paddle moves UP then DOWN |
Vision
| Test | Procedure | Pass Condition |
|---|---|---|
| AprilTag detection | Point at goal | hasBotPose() == true |
| Distance calculation | Known distance to goal | getDistanceToGoal() ±10% |
| tx accuracy | Center robot on goal | tx ≈ 0 |
Level 3: Integration Tests
Goal: Verify coordinated multi-subsystem behaviors.
| Test | Procedure | Pass Condition |
|---|---|---|
| LOAD_ALL | Insert balls, run LOAD_ALL | Balls advance, count accurate, stops when full |
| TARGETING | Face away from goal, trigger TARGETING | Robot turns to face goal (tx → 0) |
| LAUNCH_ALL | Load balls, trigger LAUNCH_ALL | All balls launched at correct speed |
| Drive + Intake | Drive while intaking | Both work simultaneously |
Level 4: Mission Tests
Goal: Verify complete autonomous mission sequences work end-to-end.
These tests require significant field space (typically half-field minimum) and are designed for practice sessions rather than pit diagnostics.
| Test | Procedure | Pass Condition |
|---|---|---|
| LaunchPreloads | Load 3 balls, place at start position, run mission | Robot navigates to launch position, aims, fires all 3 |
| BALLGroup(1) | Start after preloads, run BALLGroup with N=1 | Robot intakes group 1, returns, launches |
| OpenSesame | Position robot, run mission | Robot navigates to gate, backs in, releases classifier |
| Navigation accuracy | Run navigation-only portion | Robot reaches target pose within ±2 inches, ±3° |
| Alliance switching | Run same mission for RED then BLUE | Paths mirror correctly, correct goal targeted |
| Abort/recovery | Interrupt mission mid-execution | Robot stops safely, can restart mission |
| Mission chaining | Run full autonomous sequence | LaunchPreloads → BALLGroup(1) → OpenSesame completes |
Test Environment Requirements:
- Half-field or full-field setup
- AprilTags mounted at correct positions
- Balls available for intake tests
- Timer to verify mission completes within autonomous period (30 seconds)
Diagnostic vs Practice: Mission tests blur the line between diagnostics and practice runs. The key difference:
- Diagnostic mode: Verify the mission can work (single successful run)
- Practice mode: Verify the mission works reliably (multiple runs, gather statistics)
Current Implementation Status
The tables above describe the ideal test procedures. The Lebot2Diagnostics.java implementation uses a dual-initialization approach:
| Level | Initialization | Implementation Status | Notes |
|---|---|---|---|
| L0: Existence | Direct hardware | ✅ Complete | hardwareMap.get() lookups |
| L1: Hardware | Direct hardware | ✅ Complete | Direct motor/sensor operations |
| L2: Subsystem | Via Robot | 🔄 In Progress | Uses robot.intake.setBehavior() etc. |
| L3: Integration | Via Robot | 🔄 In Progress | Tests coordinated behaviors |
| L4: Missions | Via Robot | ✅ Ready | Uses robot.missions.start*() methods |
Architecture:
public class Lebot2Diagnostics extends OpMode {
// L0-L1: Direct hardware references (isolated from subsystem code)
private DcMotorEx leftRear, rightRear, intake, conveyor, shooter;
private Servo paddle;
private DistanceSensor frontDist, backDist;
// L2+: Full Robot (same init as competition OpMode)
private Robot robot;
private boolean robotInitialized = false;
@Override
public void init() {
// Direct hardware always available for L0-L1
initDirectHardware();
}
private void initRobotIfNeeded() {
if (!robotInitialized && currentTest.level.ordinal() >= TestLevel.LEVEL_2_SUBSYSTEM.ordinal()) {
robot = new Robot(hardwareMap, telemetry);
robotInitialized = true;
}
}
}
L2 test example (real subsystem):
private void runIntakeBehaviorTest() {
initRobotIfNeeded();
robot.intake.setBehavior(Intake.Behavior.INTAKE);
runRobotUpdateLoop(1.0); // Run for 1 second
// Verify via subsystem state
if (robot.intake.getBehavior() == Intake.Behavior.INTAKE) {
currentTest.pass("INTAKE behavior active");
} else {
currentTest.fail("Behavior not set correctly");
}
}
What’s NOT yet implemented:
Missionclass - Missions are documented concepts, not working code- Some Robot behaviors (ROLLTAKE, SHIMYTAKE) - Referenced in docs, may not exist in code
See Roadmap for remaining work.
Diagnostic OpMode Design
Current Implementation
The diagnostic OpMode (Lebot2Diagnostics.java) implements a layered test framework with safety features for competition pit use.
Test Structure
public class DiagnosticTest {
public final String name;
public final TestLevel level;
public final MotionRequirement motion; // NONE, ELEVATED, GROUND_SHORT, GROUND_MEDIUM
public final String instruction; // Shown to operator before test
public final String[] crossValidation; // Other hardware that validates this test
public TestState state; // NOT_RUN, WAITING_PERMISSION, RUNNING, PASS, FAIL, ABORTED, SKIPPED
public String resultMessage;
public double durationMs;
}
Motion Requirements
Tests are tagged with their motion requirements to ensure safety:
| Requirement | Description | Example Tests |
|---|---|---|
NONE |
No robot motion, safe anytime | Existence checks, sensor reads |
ELEVATED |
Robot wheels must be off ground | Drive motor spin tests |
GROUND_SHORT |
Robot on ground, <0.5m motion | Drive forward test |
GROUND_MEDIUM |
Robot on ground, <1.5m motion | PID turn, targeting |
Cross-Validation
Tests can validate multiple things simultaneously:
- Drive forward test validates: both encoders change, Pinpoint position changes
- Turn test validates: encoder differential, Pinpoint heading change
- This helps isolate failures (e.g., “encoders work but Pinpoint doesn’t” = I2C issue)
Safety Features
- Operator Permission Required
- Tests with motion wait for operator confirmation (A button)
- Instruction text explains what will happen
- Operator can skip test (B button) if conditions aren’t safe
- Universal Abort
- B button immediately stops all motors during any motion test
- Test marked as ABORTED, not FAIL (distinguishes operator choice from failure)
- Operator Instructions
- Each test shows what to do before running
- Examples: “ELEVATE ROBOT - wheels must be off ground”
- Examples: “Robot will drive FORWARD ~30cm. Clear path.”
- Short Motion Bounds
- All motion tests limited to <1.5m
- Designed for pit/practice field constraints
- Can be run in limited space at competitions
User Interface
- D-pad Up/Down: Select test level
- D-pad Left/Right: Select specific test within level
- A (Green): Run selected test / Confirm proceed
- B (Red): ABORT current test / Skip test
- X (Blue): Run all tests in current level
- Y (Yellow): Run full POST (all levels 0-3)
Output Format
=== LEBOT2 DIAGNOSTICS ===
Level: 1 - HARDWARE FUNCTION
Test: leftRear forward
>>> leftRear spin <<<
State: WAITING_PERMISSION
INSTRUCTION:
ELEVATE ROBOT - wheels must be off ground. Press A when ready.
[A] Proceed [B] Skip
After tests run:
=== LEBOT2 DIAGNOSTICS ===
Level: 1 - HARDWARE FUNCTION
>> [OK] leftRear spin - Fwd/Rev OK (±50 ticks)
[OK] rightRear spin - Fwd/Rev OK (±50 ticks)
[OK] intake spin - Motor ran (visual confirm)
[!!] shooter spinup - Timeout at 312 deg/s (target 500)
[--] paddle move - skipped
Summary: 3 PASS, 1 FAIL, 1 SKIP, 5 pending
Test Implementation Patterns
Multi-Phase Tests
Complex tests use a phase state machine:
private void runMotorSpinTest(DcMotorEx motor) {
switch (testPhase) {
case 0: // Start forward
capturedEncoderStart = motor.getCurrentPosition();
motor.setPower(0.3);
testPhase = 1;
break;
case 1: // Wait forward, then reverse
if (testTimer.seconds() > 0.5) {
// Check forward result, start reverse
testPhase = 2;
}
break;
case 2: // Check reverse result
// Pass or fail based on encoder changes
break;
}
}
Subsystem Tests
Level 2+ tests that need full subsystem initialization are marked as “requires full Robot init” and should be run via TeleOp rather than the standalone diagnostics OpMode.
Competition Checklist
Pre-Match Quick POST (30 seconds)
Run before every match:
- Voltage > 12.5V
- All 5 motors respond to brief pulse (6 when shooter2 added)
- Both distance sensors reading valid range
- Pinpoint heading changes when robot rotated
- Limelight sees AprilTag (if in view)
Pit Diagnostic (5 minutes)
Run after any concerning behavior:
- Full Level 0 (existence)
- Full Level 1 (hardware function)
- Specific Level 2 tests for failing subsystem
Major Reset (15 minutes)
Run after crashes, cable changes, or code updates:
- Full Level 0-3 POST
- Record results for comparison
- Verify against known-good baseline
Future Enhancements
Automated Baseline Recording
Save “known good” test results and compare against them:
// After successful competition
diagnostics.saveBaseline("competition_2026_01_15");
// Before next event
diagnostics.compareToBaseline("competition_2026_01_15");
// Output: "frontDist: was 4.2cm, now 847.3cm - CABLE DISCONNECTED?"
Continuous Background Monitoring
Run lightweight checks during teleop:
// In Robot.update()
if (loopCount % 100 == 0) { // Every ~2 seconds
healthCheck.verifyMotorCurrents();
healthCheck.verifyDistanceSensorsInRange();
healthCheck.verifyPinpointUpdating();
}
Telemetry Integration
Show health status on dashboard during matches:
HEALTH: [OK] [OK] [OK] [!!] [OK]
DRV INT LDR LCH VIS
LCH: shooter velocity low (812 vs 935 target)
Roadmap: Real Subsystem Testing
This roadmap outlines the remaining steps to complete the diagnostic framework.
Architecture Decision: Full Robot Init for L2+
We chose full Robot initialization for L2+ tests rather than partial subsystem init:
Rationale:
- Tests the same code path as competition (no hidden bugs)
- No maintenance drift - hardware names/configs come from subsystems
- L3+ tests require Robot anyway for coordinated behaviors
- L0-L1 still provide isolated hardware testing if subsystem code is broken
Trade-off accepted:
- Slower init for L2+ tests (~2-3 seconds)
- If Robot init fails, L2+ tests can’t run (but L0-L1 still work)
Phase 1: Robot Integration (✅ In Progress)
Goal: L2+ tests use robot.subsystem.setBehavior() instead of direct motor control.
Implementation pattern:
private Robot robot;
private boolean robotInitialized = false;
private void initRobotIfNeeded() {
if (!robotInitialized) {
robot = new Robot(hardwareMap, telemetry);
robotInitialized = true;
}
}
private void runRobotUpdateLoop(double seconds) {
ElapsedTime timer = new ElapsedTime();
while (timer.seconds() < seconds && !isAborted()) {
robot.update();
}
}
Status:
- Architecture defined (L0-L1 direct, L2+ via Robot)
- Add Robot field and lazy initialization to diagnostics
- Convert L2 tests to use subsystem behaviors
- Convert L3 tests to use Robot articulations
- Add
runRobotUpdateLoop()helper
Phase 2: Create Mission Class
Goal: Implement the Mission class so L4 tests can use real mission code.
File: lebot2/Mission.java (or lebot2/Missions.java)
public class Missions {
private final Robot robot;
public enum Mission {
NONE,
LAUNCH_PRELOADS,
BALL_GROUP,
OPEN_SESAME
}
public enum MissionState {
IDLE,
RUNNING,
COMPLETE,
FAILED,
ABORTED
}
private Mission currentMission = Mission.NONE;
private MissionState state = MissionState.IDLE;
private int ballGroupTarget = 0; // For parameterized BALLGroup
public Missions(Robot robot) {
this.robot = robot;
}
public void startLaunchPreloads() {
currentMission = Mission.LAUNCH_PRELOADS;
state = MissionState.RUNNING;
// Initialize mission state machine
}
public void startBALLGroup(int targetBalls) {
currentMission = Mission.BALL_GROUP;
ballGroupTarget = targetBalls;
state = MissionState.RUNNING;
}
public void update() {
if (state != MissionState.RUNNING) return;
switch (currentMission) {
case LAUNCH_PRELOADS:
updateLaunchPreloads();
break;
case BALL_GROUP:
updateBALLGroup();
break;
// ...
}
}
public void abort() {
state = MissionState.ABORTED;
robot.emergencyStop();
}
public MissionState getState() { return state; }
public Mission getCurrentMission() { return currentMission; }
}
Dependencies:
- Requires Robot behaviors (TARGETING, LAUNCH_ALL) to be working
- Requires path following for navigation portions
- May need field configuration (starting position, ball locations)
Phase 3: Implement Missing Robot Behaviors
Goal: Implement ROLLTAKE, SHIMYTAKE, and other documented behaviors.
| Behavior | Description | Subsystems Coordinated |
|---|---|---|
| ROLLTAKE | Drive + intake simultaneously | DriveTrain, Intake, Loader |
| SHIMYTAKE | Shimmy motion while intaking (for stuck balls) | DriveTrain, Intake |
| TARGETING | Auto-aim at goal using vision | DriveTrain, Vision |
| LAUNCH_ALL | Fire all loaded balls | Launcher, Loader |
Implementation order:
- TARGETING - Most useful, enables autonomous aiming
- LAUNCH_ALL - Core game functionality
- ROLLTAKE - Enhances ball collection
- SHIMYTAKE - Edge case handling
Phase 4: L3/L4 Test Implementation
Goal: Replace placeholder tests with real behavior/mission invocations.
// L3: Integration test using real behaviors
private void runTargetingTest() {
robot.setArticulation(Robot.Articulation.TARGETING);
// Run update loop until targeting completes or timeout
while (testTimer.seconds() < TARGETING_TIMEOUT) {
robot.update();
if (Math.abs(robot.vision.getTx()) < 2.0) {
currentTest.pass("Targeting converged: tx=" + robot.vision.getTx());
return;
}
}
currentTest.fail("Targeting timeout");
}
// L4: Mission test using Missions.java
private void runLaunchPreloadsTest() {
// Start the mission
robot.missions.startLaunchPreloads();
while (testTimer.seconds() < MISSION_TIMEOUT) {
// Robot.update() calls missions.calc() when active
robot.update(null);
if (robot.missions.isComplete()) {
currentTest.pass("LaunchPreloads mission complete");
return;
}
if (robot.missions.isFailed()) {
currentTest.fail("Mission failed");
return;
}
}
robot.missions.abort();
currentTest.fail("Mission timeout");
}
// L4: Ball group mission test
private void runBallGroupTest() {
robot.missions.startBallGroup(0); // Test group 0
while (testTimer.seconds() < MISSION_TIMEOUT) {
robot.update(null);
if (robot.missions.isComplete()) {
currentTest.pass("BallGroup mission complete, balls: " + robot.loader.getBallCount());
return;
}
if (robot.missions.isFailed()) {
currentTest.fail("BallGroup mission failed");
return;
}
}
robot.missions.abort();
currentTest.fail("BallGroup timeout");
}
Implementation Priority
| Priority | Task | Effort | Impact |
|---|---|---|---|
| 1 | Phase 1: Robot init + L2 test conversion | Medium | Enables real subsystem tests |
| 2 | Phase 1: L3 test conversion | Low | Enables integration tests |
| 3 | ✅ Done | Missions.java implemented | |
| 4 | Phase 4: L4 test conversion | Medium | Wire diagnostics to robot.missions.* |
| 5 | Field position calibration | High | Real coordinates for BallGroup/OpenSesame |
| 6 | GO_BALL_CLUSTER implementation | Medium | Vision-based cluster pickup |
Success Criteria
The diagnostics framework is “complete” when:
- L0-L1 tests use direct hardware (isolated from subsystem code)
- L2+ tests use full Robot initialization (same as competition)
- L2 tests call
robot.subsystem.setBehavior()and verify state - L3 tests use
robot.setBehavior()for coordinated behaviors - L4 tests can invoke
robot.missions.*methods (Missions.java implemented) - All tests validate outcomes via subsystem state/telemetry, not just “it ran”
Appendix: Test Inventory Summary
| Level | Test Count | Motion Tests | Pit-Safe |
|---|---|---|---|
| L0: Existence | 11 | 0 | Yes |
| L1: Hardware | 10 | 4 (elevated) | Yes |
| L2: Subsystem | 7 | 3 | Mostly |
| L3: Integration | 4 | 3 | Limited |
| L4: Missions | 7 | All | No (field required) |
Last updated: January 2026
