Skip to content
Apex Robotics

[ 05 / CODE ]

How APEX-R1 thinks.

Java on the FTC® SDK v12.0. One loop, six steps, four gates.

DWG
DWG-005
REV
A
UPDATED
2026-10-12
SDK
v12.0 · 20260907
Language
Java 8 target
TeamCode
30 files · 5,495 lines

[ 01 / CONSOLE ]

$

The real build, as recorded in the team's setup guide. It compiles without a robot attached, from VS Code or Cursor with adb — no Android Studio needed.

Sourcedocs/00-START-HERE.md (opens on GitHub in a new tab)

[ 02 / THE LOOP ]

Once per loop, in this order.

TeleOpMain calls each sensor's update() exactly once per loop, in dependency order — odometry, colour, vision — then solves the shot, drives the turret and flywheel, and asks the four gates.

LOOP 0000Once per loop, in this order · shown slowed down

  1. 01

    Odometry

    Odometry.update()

    Reads the Pinpoint: where the robot is, how fast it moves, how fast it turns.

    Hands on: pose · field velocity · rotation rate

  2. 02

    BallSorter

    BallSorter.update()

    Colour sensor says which ball is loaded, which picks the RPM curve.

    Hands on: ball type → RPM curve

  3. 03

    HiveTracker

    HiveTracker.update()

    Turns the AprilTag Cluster into a target in field coordinates, bridged by odometry when vision drops out.

    Hands on: target position (field frame)

  4. 04

    Ballistics

    Ballistics.solve()

    Solves the moving shot: aims at a virtual target displaced by −velocity × flight time.

    Hands on: turret angle · flywheel RPM · flight time

  5. 05

    Turret + Shooter

    Turret.setTargetFieldAngleRad() + Shooter.setTargetRpm()

    Turret holds the field angle with heading feedforward; the flywheel closes the loop on RPM.

    Hands on: on target · at speed

  6. 06

    AimController

    AimController.isReadyToFire()

    Four gates must all pass before the feeder runs — never a timer.

    Hands on: READY → feeder

[ 03 / SENSORS ]

BIOBUZZ™ moves the target.

The usual FTC pattern finds the robot with AprilTags and aims at a fixed goal. This season's game, BIOBUZZ presented by RTX, breaks both halves: the HIVE tips, and its tags move with it. So odometry finds the robot, and vision measures the target.

“Unfortunately, since BIOBUZZ AprilTags move, they are not suitable for absolute Field Localization.”

FTC SDK 12.0 release notes, quoted in docs/03-ARCHITECTURE.md

Not a downgrade: each cluster's origin sits at the centre of the CELL opening, so the pose vision returns already is the aim point.

Where is the robot?
UsualAprilTags (absolute)BIOBUZZ™Odometry (Pinpoint)⇄ swapped
Where is the target?
UsualHard-coded field constantBIOBUZZ™Vision — measured live⇄ swapped
How fast are we going?
UsualOdometryBIOBUZZ™Odometry= same

[ 04 / SOLVER ]

Aim at a virtual target.

A ball leaving a moving robot keeps the robot's velocity, so the turret aims at the target minus velocity × flight time. With the hood fixed at 40°, the flight time falls straight out of the geometry. Change the range and the robot's speed — every number here comes from the team's solver.

Fig. 4 · One shot, two viewsTrue scale · views aligned on range
  • Ball
  • Turret aim
  • v·t the robot adds
  • Every 50 ms

Range to the CELL

96in

8 ft 0 in

0in/sstopped

+30in/sleft

Solution · valid

Iter 4/6

Flywheel

rpm = 60·v / (η·π·D)

Flywheel RPM: 2,391rpm

Time of flight
0.507s
Muzzle speed
250.4in/s
Turret
−9.0°robot frame
Range
96.0into the CELL
Virtual d
97.2inwhat we aim at
Lead
−9.0°
Apex
47.6inat 0.417 s

Solution valid.

The maths, with these numbers.

θ = 40° hood · g = 386.1 in/s² · d = virtual distance

01 · Flight time · fixed hood

t = √(2(d·tanθ − h) / g)

= √(2(97.2 × 0.839 − 32.0) / 386.1)

t = 0.507 s · after 4 passes

02 · Muzzle speed

v = (d / t) / cosθ

= (97.2 / 0.507) / 0.766

v = 250.4 in/s

03 · Flywheel · this ball

rpm = 60·v / (η·π·D)

= 60 × 250.4 / (0.50 × π × 4.0)

rpm = 2,391

The code, following along.

Verbatim from the repo · trimmed where marked ⋮

The solver looputil/Ballistics.java · L135–159 (opens the file on GitHub in a new tab)
for (int i = 0; i < 6; i++) {// 4 of 6 passes    // Where the target effectively is, given that the ball inherits our velocity.    double vtx = targetX - robotVx * t;    double vty = targetY - robotVy * t;     double dx = vtx - muzzleX;    double dy = vty - muzzleY;    d = Math.hypot(dx, dy);    azimuth = Math.atan2(dy, dx);     // t^2 = 2*(d*tan(theta) - h)/G. If the bracket is non-positive the target sits at or    // above the launch line, so no shot exists at this hood angle.    double bracket = d * tanTheta - h;    if (bracket <= 0) {        // … failReason: target too high/close for the hood        return s;    }     double tNew = Math.sqrt(2.0 * bracket / G_IN_S2);    boolean converged = Math.abs(tNew - t) < 1e-4;    t = tNew;    if (converged) break;// |Δt| 2.0e-6 s}

This solve, pass by pass

stop when |Δt| < 1e-4 s

Passt inVirtual target (x, y)dt out|Δt|
10.0000 s(96.0, 0.0)96.0 in0.5015 s5.0e-1
20.5015 s(96.0, -15.0)97.2 in0.5066 s5.1e-3
30.5066 s(96.0, -15.2)97.2 in0.5067 s1.0e-4
40.5067 s(96.0, -15.2)97.2 in0.5067 s2.0e-6✓ converged
muzzleSpeedToRpm()util/Ballistics.java · L220–224 (opens the file on GitHub in a new tab)
public static double muzzleSpeedToRpm(double muzzleSpeedIn, BallType ball) {    double eff = ball.transferEfficiency > 1e-6 ? ball.transferEfficiency : 0.5;// η = 0.50    double circumference = Math.PI * RobotConstants.Shoot.FLYWHEEL_DIAMETER_IN;    return 60.0 * muzzleSpeedIn / (eff * circumference);// → 2,391 rpm}
BallTypeutil/BallType.java · L37–56 (opens the file on GitHub in a new tab)
public enum BallType {     /** Yellow, 2.8 in, 25 g. Either alliance may score it. */    POLLEN(2.8, 0.025, 0.50, 0.0),     /** Red 3.6 in, 41 g. Only the RED alliance may score it. */    NECTAR_RED(3.6, 0.041, 0.42, 0.0),     /** Blue 3.6 in, 41 g. Only the BLUE alliance may score it. */    NECTAR_BLUE(3.6, 0.041, 0.42, 0.0),     // … NONE(0, 0, 0.0, 0.0): an unclassified ball refuses to fire    NONE(0, 0, 0.0, 0.0);}

NECTAR is 1.65× the mass and 1.29× the diameter of POLLEN, so less of the wheel's speed reaches the ball (η 0.42 vs 0.50). Same required ball speed, about 19% more RPM — which is why the colour sensor picks the RPM curve.

Muzzle 14 in up, 2 in ahead of the tracking point (RobotConstants.Shoot). Aim point 46 in up is an estimate — the HIVE pivots at 43.95 in (manual TU04 §9.6); on the robot, HiveTracker measures it live. Drag-free: BallType.dragPerInch stays 0 until tuned, and the file warns the drag-free range is roughly 2× optimistic.

[ 05 / GATES ]

Four gates, then fire.

The trigger is a request, not a command. The feeder runs only when isReadyToFire() is true — checked every loop, never on a timer. Flip the conditions and watch where the check stops.

Preconditions

The four gates

All of them, every loop →

Not readyBlocked at ON TARGET

Driver telemetry · getReadinessText()

>>> TURRET ±n.n deg <<<

trigger > 0.5 && isReadyToFire()

isReadyToFire()AimController.java · L134–141 (opens the file on GitHub in a new tab)
public boolean isReadyToFire() {    return aimingEnabled            && assumedBall.canFire()            && solution.valid            && turret.isOnTarget()// false → stop here            && shooter.isAtSpeed()// never evaluated            && hiveTracker.hasTarget();}
Why four, not one
On a robot that is also driving, any one of them can be false without the driver noticing. The code checks them and lights one indicator.
Why at speed means settled
A wheel spinning up passes through its target on the way to overshooting. A ball fed then gets the right speed for a millisecond.
Short-circuit
Java's && stops at the first false, so the lines after it never run. The listing shows where this loop's check stopped.
Why it matters
Under the provisional scoring a HIVE TIP is worth 20 points, an element in a CELL 2. Tips need balls in the CELL, and a ball fired before the wheel settled misses it.

[ 06 / CODEBASE ]

Explore the codebase.

30 Java files, 5,495 lines, all in TeamCode/ — and every tunable number in one of them, RobotConstants.java. Open a folder, pick a file.

[ 07 / TUNING ]

Tune in this order.

“Do not skip ahead. Every step depends on the one before it being right.” Each step brings in one more piece of hardware — read the matrix downward and the robot assembles itself.

  1. T0

    T0 Tag Library Dump

    Confirms the SDK knows the BIOBUZZ™ tag library.

    Driver Station: “T0 Tag Library Dump (no hardware)”

    Adds Control Hub· T0_TagLibraryDump.java

    Adds Control Hub.
  2. T1

    T1 Odometry Test

    Pinpoint found, pose updating.

    Adds Pinpoint· T1_OdometryTest.java

    Adds Pinpoint.
  3. T2

    T2 Pinpoint Offset Tuner

    Measures the pod offsets.

    Pinpoint· T2_PinpointOffsetTuner.java

    Hardware: Pinpoint.
  4. T3

    T3 Velocity Frame Test

    Proves whether velocity is field- or robot-relative.

    Pinpoint· T3_VelocityFrameTest.java

    Hardware: Pinpoint.
  5. DRV

    TeleOp Drive Only

    Motor directions and strafing.

    Adds 4 drive motors· TeleOpDriveOnly.java

    Adds 4 drive motors.
  6. T4

    T4 Hive Tracker Test

    Cluster detection and dropout bridging.

    Adds Webcam· T4_HiveTrackerTest.java

    Adds Webcam.
  7. T5

    T5 Shooter RPM Tuner

    Transfer efficiency from a flat shot.

    Adds Flywheel· T5_ShooterRpmTuner.java

    Adds Flywheel.
  8. T6

    T6 Color Sensor Calibration

    POLLEN / NECTAR hue windows.

    Adds Colour sensor· T6_ColorSensorCalibration.java

    Adds Colour sensor.
  9. MAIN

    TeleOp Main@Disabled until the hardware exists

    Everything together.

    Adds Turret, feeder, intake· TeleOpMain.java

    Adds Turret, feeder, intake.

“Steps 1 and 2 are where the season is won.” Odometry and vision come first: a turret with perfect ballistics on odometry that drifts two inches a lap will miss, and the ballistics will get the blame.— docs/03-ARCHITECTURE.md

[ 08 / RULES + STACK ]

Rules of the road.

Five notes from the team README that keep twelve people from breaking each other's code.

Notes

  1. 01

    Only ever edit TeamCode/. FtcRobotController/ belongs to FIRST; changing it breaks SDK updates.

  2. 02

    All tunable numbers live in RobotConstants.java. No magic numbers in subsystems.

  3. 03

    One convention everywhere: +X forward, +Y left, +heading counter-clockwise, inches and radians.

  4. 04

    Call each sensor's update() exactly once per loop, near the top, in dependency order: odometry → colour → vision → aim.

  5. 05

    Pull SDK updates, don't re-clone: git fetch upstream --tags && git merge v12.x.

Stack

Language
Java 8 target (OnBot-compatible)
SDK
FTC SDK 12.0
Build
Gradle 9.1 · AGP 8.13.2
Paths
Pedro Pathing
Odometry
goBILDA Pinpoint
Vision
AprilTag Clusters · Limelight 3A (optional)
Editor
VS Code / Cursor + adb
SDK build
FTC SDK v12.0 (20260907-090034)

[ 09 / REPO ]

Read the code.

Start with TEAM-README.md, then docs/00-START-HERE.md. Everything on this page links back to the file it came from.

git clone https://github.com/aaaravM/ApexFTC2026---36674.git