[ 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.
[ 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
- 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
- 02
BallSorter
BallSorter.
update() Colour sensor says which ball is loaded, which picks the RPM curve.
Hands on: ball type → RPM curve
- 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)
- 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
- 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
- 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.”
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.
A · Elevation
B · Plan, looking down
+Y left · +X toward the CELL
- Ball
- Turret aim
- v·t the robot adds
- Every 50 ms
Range to the CELL
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 ⋮
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
| Pass | t in | Virtual target (x, y) | d | t out | |Δt| |
|---|---|---|---|---|---|
| 1 | 0.0000 s | (96.0, 0.0) | 96.0 in | 0.5015 s | 5.0e-1 |
| 2 | 0.5015 s | (96.0, -15.0) | 97.2 in | 0.5066 s | 5.1e-3 |
| 3 | 0.5066 s | (96.0, -15.2) | 97.2 in | 0.5067 s | 1.0e-4 |
| 4 | 0.5067 s | (96.0, -15.2) | 97.2 in | 0.5067 s | 2.0e-6✓ converged |
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}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 →
Driver telemetry · getReadinessText()
>>> TURRET ±n.n deg <<<
trigger > 0.5 && isReadyToFire()
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.
- T0Adds Control Hub.
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
- T1Adds Pinpoint.
T1 Odometry Test
Pinpoint found, pose updating.
Adds Pinpoint· T1_OdometryTest.java
- T2Hardware: Pinpoint.
T2 Pinpoint Offset Tuner
Measures the pod offsets.
Pinpoint· T2_PinpointOffsetTuner.java
- T3Hardware: Pinpoint.
T3 Velocity Frame Test
Proves whether velocity is field- or robot-relative.
Pinpoint· T3_VelocityFrameTest.java
- DRVAdds 4 drive motors.
TeleOp Drive Only
Motor directions and strafing.
Adds 4 drive motors· TeleOpDriveOnly.java
- T4Adds Webcam.
T4 Hive Tracker Test
Cluster detection and dropout bridging.
Adds Webcam· T4_HiveTrackerTest.java
- T5Adds Flywheel.
T5 Shooter RPM Tuner
Transfer efficiency from a flat shot.
Adds Flywheel· T5_ShooterRpmTuner.java
- T6Adds Colour sensor.
T6 Color Sensor Calibration
POLLEN / NECTAR hue windows.
Adds Colour sensor· T6_ColorSensorCalibration.java
- MAINAdds Turret, feeder, intake.
TeleOp Main@Disabled until the hardware exists
Everything together.
Adds Turret, feeder, intake· TeleOpMain.java
“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.
Drawn from
Notes
- 01
Only ever edit
TeamCode/.FtcRobotController/belongs to FIRST; changing it breaks SDK updates. - 02
All tunable numbers live in
RobotConstants.java. No magic numbers in subsystems. - 03
One convention everywhere:
+X forward, +Y left, +heading counter-clockwise, inches and radians. - 04
Call each sensor's
update()exactly once per loop, near the top, in dependency order: odometry → colour → vision → aim. - 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