ftc-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1800 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXAMPLES = void 0;
4
+ exports.EXAMPLES = {
5
+ "pedro-auto": `package org.firstinspires.ftc.teamcode;
6
+
7
+ import com.acmerobotics.dashboard.FtcDashboard;
8
+ import com.acmerobotics.dashboard.config.Config;
9
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
10
+ import com.acmerobotics.dashboard.canvas.Canvas;
11
+ import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
12
+ import com.pedropathing.follower.Follower;
13
+ import com.pedropathing.localization.Pose;
14
+ import com.pedropathing.pathgen.BezierCurve;
15
+ import com.pedropathing.pathgen.BezierLine;
16
+ import com.pedropathing.pathgen.PathChain;
17
+ import com.pedropathing.pathgen.Point;
18
+ import com.pedropathing.util.Timer;
19
+ import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
20
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
21
+
22
+ @Config
23
+ @Autonomous(name = "Pedro Auto - 3 Sample", group = "Auto")
24
+ public class PedroAutoExample extends OpMode {
25
+
26
+ // --- Dashboard-tunable poses ---
27
+ public static double START_X = 9.0;
28
+ public static double START_Y = 60.0;
29
+ public static double START_HEADING = Math.toRadians(0);
30
+
31
+ public static double SCORE_X = 38.0;
32
+ public static double SCORE_Y = 68.0;
33
+ public static double SCORE_HEADING = Math.toRadians(0);
34
+
35
+ public static double SAMPLE1_X = 37.0;
36
+ public static double SAMPLE1_Y = 30.0;
37
+ public static double SAMPLE1_HEADING = Math.toRadians(0);
38
+
39
+ public static double SAMPLE2_X = 37.0;
40
+ public static double SAMPLE2_Y = 20.0;
41
+ public static double SAMPLE2_HEADING = Math.toRadians(0);
42
+
43
+ public static double SAMPLE3_X = 44.0;
44
+ public static double SAMPLE3_Y = 20.0;
45
+ public static double SAMPLE3_HEADING = Math.toRadians(30);
46
+
47
+ public static double PARK_X = 10.0;
48
+ public static double PARK_Y = 10.0;
49
+ public static double PARK_HEADING = Math.toRadians(0);
50
+
51
+ // Control point for curved paths
52
+ public static double CURVE_CONTROL_X = 20.0;
53
+ public static double CURVE_CONTROL_Y = 40.0;
54
+
55
+ private Follower follower;
56
+ private Timer pathTimer;
57
+ private int pathState;
58
+
59
+ // Path chains
60
+ private PathChain scorePreload, grabSample1, scoreSample1;
61
+ private PathChain grabSample2, scoreSample2;
62
+ private PathChain grabSample3, scoreSample3;
63
+ private PathChain park;
64
+
65
+ private void buildPaths() {
66
+ Pose startPose = new Pose(START_X, START_Y, START_HEADING);
67
+ Pose scorePose = new Pose(SCORE_X, SCORE_Y, SCORE_HEADING);
68
+ Pose sample1Pose = new Pose(SAMPLE1_X, SAMPLE1_Y, SAMPLE1_HEADING);
69
+ Pose sample2Pose = new Pose(SAMPLE2_X, SAMPLE2_Y, SAMPLE2_HEADING);
70
+ Pose sample3Pose = new Pose(SAMPLE3_X, SAMPLE3_Y, SAMPLE3_HEADING);
71
+ Pose parkPose = new Pose(PARK_X, PARK_Y, PARK_HEADING);
72
+
73
+ follower.setStartingPose(startPose);
74
+
75
+ scorePreload = follower.pathBuilder()
76
+ .addPath(new BezierLine(
77
+ new Point(startPose),
78
+ new Point(scorePose)))
79
+ .setLinearHeadingInterpolation(startPose.getHeading(), scorePose.getHeading())
80
+ .build();
81
+
82
+ grabSample1 = follower.pathBuilder()
83
+ .addPath(new BezierCurve(
84
+ new Point(scorePose),
85
+ new Point(CURVE_CONTROL_X, CURVE_CONTROL_Y, Point.CARTESIAN),
86
+ new Point(sample1Pose)))
87
+ .setLinearHeadingInterpolation(scorePose.getHeading(), sample1Pose.getHeading())
88
+ .build();
89
+
90
+ scoreSample1 = follower.pathBuilder()
91
+ .addPath(new BezierLine(
92
+ new Point(sample1Pose),
93
+ new Point(scorePose)))
94
+ .setLinearHeadingInterpolation(sample1Pose.getHeading(), scorePose.getHeading())
95
+ .build();
96
+
97
+ grabSample2 = follower.pathBuilder()
98
+ .addPath(new BezierLine(
99
+ new Point(scorePose),
100
+ new Point(sample2Pose)))
101
+ .setLinearHeadingInterpolation(scorePose.getHeading(), sample2Pose.getHeading())
102
+ .build();
103
+
104
+ scoreSample2 = follower.pathBuilder()
105
+ .addPath(new BezierLine(
106
+ new Point(sample2Pose),
107
+ new Point(scorePose)))
108
+ .setLinearHeadingInterpolation(sample2Pose.getHeading(), scorePose.getHeading())
109
+ .build();
110
+
111
+ grabSample3 = follower.pathBuilder()
112
+ .addPath(new BezierCurve(
113
+ new Point(scorePose),
114
+ new Point(CURVE_CONTROL_X, CURVE_CONTROL_Y, Point.CARTESIAN),
115
+ new Point(sample3Pose)))
116
+ .setLinearHeadingInterpolation(scorePose.getHeading(), sample3Pose.getHeading())
117
+ .build();
118
+
119
+ scoreSample3 = follower.pathBuilder()
120
+ .addPath(new BezierLine(
121
+ new Point(sample3Pose),
122
+ new Point(scorePose)))
123
+ .setLinearHeadingInterpolation(sample3Pose.getHeading(), scorePose.getHeading())
124
+ .build();
125
+
126
+ park = follower.pathBuilder()
127
+ .addPath(new BezierCurve(
128
+ new Point(scorePose),
129
+ new Point(CURVE_CONTROL_X, CURVE_CONTROL_Y, Point.CARTESIAN),
130
+ new Point(parkPose)))
131
+ .setLinearHeadingInterpolation(scorePose.getHeading(), parkPose.getHeading())
132
+ .build();
133
+ }
134
+
135
+ private void setPathState(int state) {
136
+ pathState = state;
137
+ pathTimer.resetTimer();
138
+ }
139
+
140
+ private void autonomousPathUpdate() {
141
+ switch (pathState) {
142
+ case 0: // Drive to score preloaded sample
143
+ if (!follower.isBusy()) {
144
+ follower.followPath(scorePreload, true);
145
+ setPathState(1);
146
+ }
147
+ break;
148
+
149
+ case 1: // Wait to arrive at scoring position, then "score"
150
+ if (!follower.isBusy()) {
151
+ // Score the preloaded sample (actuate mechanism here)
152
+ setPathState(2);
153
+ }
154
+ break;
155
+
156
+ case 2: // Drive to grab sample 1
157
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
158
+ follower.followPath(grabSample1, true);
159
+ setPathState(3);
160
+ }
161
+ break;
162
+
163
+ case 3: // Wait to arrive at sample 1, then grab
164
+ if (!follower.isBusy()) {
165
+ // Grab sample 1 (actuate mechanism here)
166
+ setPathState(4);
167
+ }
168
+ break;
169
+
170
+ case 4: // Drive to score sample 1
171
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
172
+ follower.followPath(scoreSample1, true);
173
+ setPathState(5);
174
+ }
175
+ break;
176
+
177
+ case 5: // Wait to arrive, then score sample 1
178
+ if (!follower.isBusy()) {
179
+ // Score sample 1
180
+ setPathState(6);
181
+ }
182
+ break;
183
+
184
+ case 6: // Drive to grab sample 2
185
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
186
+ follower.followPath(grabSample2, true);
187
+ setPathState(7);
188
+ }
189
+ break;
190
+
191
+ case 7: // Wait to arrive at sample 2, then grab
192
+ if (!follower.isBusy()) {
193
+ // Grab sample 2
194
+ setPathState(8);
195
+ }
196
+ break;
197
+
198
+ case 8: // Drive to score sample 2
199
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
200
+ follower.followPath(scoreSample2, true);
201
+ setPathState(9);
202
+ }
203
+ break;
204
+
205
+ case 9: // Wait to arrive, then score sample 2
206
+ if (!follower.isBusy()) {
207
+ // Score sample 2
208
+ setPathState(10);
209
+ }
210
+ break;
211
+
212
+ case 10: // Drive to grab sample 3
213
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
214
+ follower.followPath(grabSample3, true);
215
+ setPathState(11);
216
+ }
217
+ break;
218
+
219
+ case 11: // Wait to arrive at sample 3, then grab
220
+ if (!follower.isBusy()) {
221
+ // Grab sample 3
222
+ setPathState(12);
223
+ }
224
+ break;
225
+
226
+ case 12: // Drive to score sample 3
227
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
228
+ follower.followPath(scoreSample3, true);
229
+ setPathState(13);
230
+ }
231
+ break;
232
+
233
+ case 13: // Wait to arrive, then score sample 3
234
+ if (!follower.isBusy()) {
235
+ // Score sample 3
236
+ setPathState(14);
237
+ }
238
+ break;
239
+
240
+ case 14: // Park
241
+ if (pathTimer.getElapsedTimeSeconds() > 0.5) {
242
+ follower.followPath(park, true);
243
+ setPathState(15);
244
+ }
245
+ break;
246
+
247
+ case 15: // Done
248
+ // Autonomous complete
249
+ break;
250
+ }
251
+ }
252
+
253
+ @Override
254
+ public void init() {
255
+ pathTimer = new Timer();
256
+ pathState = 0;
257
+
258
+ follower = new Follower(hardwareMap);
259
+
260
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
261
+
262
+ buildPaths();
263
+ }
264
+
265
+ @Override
266
+ public void start() {
267
+ pathTimer.resetTimer();
268
+ setPathState(0);
269
+ }
270
+
271
+ @Override
272
+ public void loop() {
273
+ follower.update();
274
+ autonomousPathUpdate();
275
+
276
+ // Draw robot on FTC Dashboard field overlay
277
+ Pose currentPose = follower.getPose();
278
+ TelemetryPacket packet = new TelemetryPacket();
279
+ Canvas fieldOverlay = packet.fieldOverlay();
280
+
281
+ fieldOverlay.setStroke("#3F51B5");
282
+ fieldOverlay.setStrokeWidth(1);
283
+ fieldOverlay.strokeCircle(currentPose.getX(), currentPose.getY(), 9);
284
+ double arrowX = currentPose.getX() + 12 * Math.cos(currentPose.getHeading());
285
+ double arrowY = currentPose.getY() + 12 * Math.sin(currentPose.getHeading());
286
+ fieldOverlay.strokeLine(currentPose.getX(), currentPose.getY(), arrowX, arrowY);
287
+
288
+ FtcDashboard.getInstance().sendTelemetryPacket(packet);
289
+
290
+ telemetry.addData("Path State", pathState);
291
+ telemetry.addData("X", currentPose.getX());
292
+ telemetry.addData("Y", currentPose.getY());
293
+ telemetry.addData("Heading (deg)", Math.toDegrees(currentPose.getHeading()));
294
+ telemetry.addData("Busy", follower.isBusy());
295
+ telemetry.update();
296
+ }
297
+ }`,
298
+ "pedro-teleop": `package org.firstinspires.ftc.teamcode;
299
+
300
+ import com.acmerobotics.dashboard.FtcDashboard;
301
+ import com.acmerobotics.dashboard.config.Config;
302
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
303
+ import com.pedropathing.follower.Follower;
304
+ import com.pedropathing.localization.Pose;
305
+ import com.qualcomm.hardware.lynx.LynxModule;
306
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
307
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
308
+ import com.qualcomm.robotcore.util.ElapsedTime;
309
+
310
+ import java.util.List;
311
+
312
+ @Config
313
+ @TeleOp(name = "Pedro TeleOp", group = "TeleOp")
314
+ public class PedroTeleOpExample extends OpMode {
315
+
316
+ public static double NORMAL_SPEED = 1.0;
317
+ public static double SLOW_SPEED = 0.4;
318
+
319
+ private Follower follower;
320
+ private double speedMultiplier = NORMAL_SPEED;
321
+ private boolean slowModeActive = false;
322
+ private boolean previousAState = false;
323
+
324
+ private List<LynxModule> allHubs;
325
+ private ElapsedTime loopTimer;
326
+
327
+ @Override
328
+ public void init() {
329
+ // Set up manual bulk reads for performance
330
+ allHubs = hardwareMap.getAll(LynxModule.class);
331
+ for (LynxModule hub : allHubs) {
332
+ hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
333
+ }
334
+
335
+ follower = new Follower(hardwareMap);
336
+ follower.setStartingPose(new Pose(0, 0, 0));
337
+
338
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
339
+
340
+ loopTimer = new ElapsedTime();
341
+
342
+ telemetry.addData("Status", "Initialized");
343
+ telemetry.update();
344
+ }
345
+
346
+ @Override
347
+ public void start() {
348
+ follower.startTeleopDrive();
349
+ loopTimer.reset();
350
+ }
351
+
352
+ @Override
353
+ public void loop() {
354
+ // Clear bulk cache at the start of each loop
355
+ for (LynxModule hub : allHubs) {
356
+ hub.clearBulkCache();
357
+ }
358
+
359
+ // Toggle slow mode with gamepad1.a (rising edge detection)
360
+ boolean currentAState = gamepad1.a;
361
+ if (currentAState && !previousAState) {
362
+ slowModeActive = !slowModeActive;
363
+ }
364
+ previousAState = currentAState;
365
+
366
+ // Read speed multiplier from dashboard-tunable statics at point of use
367
+ speedMultiplier = slowModeActive ? SLOW_SPEED : NORMAL_SPEED;
368
+
369
+ // Drive using Pedro's built-in TeleOp drive
370
+ follower.setTeleOpMovementVectors(
371
+ -gamepad1.left_stick_y * speedMultiplier,
372
+ -gamepad1.left_stick_x * speedMultiplier,
373
+ -gamepad1.right_stick_x * speedMultiplier,
374
+ true
375
+ );
376
+ follower.update();
377
+
378
+ // Telemetry
379
+ Pose currentPose = follower.getPose();
380
+ telemetry.addData("--- Drive Info ---", "");
381
+ telemetry.addData("Slow Mode", slowModeActive ? "ON" : "OFF");
382
+ telemetry.addData("Speed Multiplier", "%.2f", speedMultiplier);
383
+ telemetry.addData("--- Pose ---", "");
384
+ telemetry.addData("X", "%.2f", currentPose.getX());
385
+ telemetry.addData("Y", "%.2f", currentPose.getY());
386
+ telemetry.addData("Heading (deg)", "%.2f", Math.toDegrees(currentPose.getHeading()));
387
+ telemetry.addData("--- Performance ---", "");
388
+ telemetry.addData("Loop Time (ms)", "%.1f", loopTimer.milliseconds());
389
+ telemetry.update();
390
+
391
+ loopTimer.reset();
392
+ }
393
+ }`,
394
+ "pedro-constants": `package org.firstinspires.ftc.teamcode;
395
+
396
+ import com.pedropathing.follower.Follower;
397
+ import com.pedropathing.follower.FollowerConstants;
398
+ import com.pedropathing.localization.constants.PinpointConstants;
399
+ import com.pedropathing.pathgen.MathFunctions;
400
+ import com.pedropathing.pathgen.PathConstraints;
401
+ import com.qualcomm.hardware.lynx.LynxModule;
402
+ import com.qualcomm.robotcore.hardware.DcMotorSimple;
403
+ import com.qualcomm.robotcore.hardware.HardwareMap;
404
+
405
+ import org.firstinspires.ftc.robotcore.external.navigation.DistanceUnit;
406
+
407
+ public class Constants {
408
+
409
+ static {
410
+ // --- Follower PID and Drive Constants ---
411
+ FollowerConstants.mass = 13.0; // robot mass in kg
412
+
413
+ FollowerConstants.translationalPIDFCoefficients.setCoefficients(0.15, 0, 0.01, 0);
414
+ FollowerConstants.headingPIDFCoefficients.setCoefficients(2.0, 0, 0.1, 0);
415
+ FollowerConstants.drivePIDFCoefficients.setCoefficients(0.01, 0, 0.0002, 0.6);
416
+
417
+ FollowerConstants.zeroPowerAcceleration = -30;
418
+ FollowerConstants.centripetalScaling = 0.0005;
419
+
420
+ FollowerConstants.useSecondaryTranslationalPID = false;
421
+ FollowerConstants.useSecondaryHeadingPID = false;
422
+ FollowerConstants.useSecondaryDrivePID = false;
423
+
424
+ // --- Mecanum Motor Configuration ---
425
+ FollowerConstants.leftFrontMotorName = "leftFront";
426
+ FollowerConstants.leftRearMotorName = "leftRear";
427
+ FollowerConstants.rightFrontMotorName = "rightFront";
428
+ FollowerConstants.rightRearMotorName = "rightRear";
429
+
430
+ FollowerConstants.leftFrontMotorDirection = DcMotorSimple.Direction.REVERSE;
431
+ FollowerConstants.leftRearMotorDirection = DcMotorSimple.Direction.REVERSE;
432
+ FollowerConstants.rightFrontMotorDirection = DcMotorSimple.Direction.FORWARD;
433
+ FollowerConstants.rightRearMotorDirection = DcMotorSimple.Direction.FORWARD;
434
+
435
+ // --- Pinpoint Localizer Configuration ---
436
+ PinpointConstants.forwardY = 5.5; // forward encoder Y offset in inches
437
+ PinpointConstants.strafeX = -3.0; // strafe encoder X offset in inches
438
+ PinpointConstants.distanceUnit = DistanceUnit.INCH;
439
+ PinpointConstants.hardwareMapName = "pinpoint";
440
+ PinpointConstants.useYawScalar = false;
441
+ PinpointConstants.yawScalar = 1.0;
442
+ PinpointConstants.useCustomEncoderResolution = false;
443
+ PinpointConstants.encoderResolution = PinpointConstants.GoBildaOdometryPods.goBILDA_4_BAR_POD;
444
+ PinpointConstants.forwardEncoderDirection = PinpointConstants.EncoderDirection.FORWARD;
445
+ PinpointConstants.strafeEncoderDirection = PinpointConstants.EncoderDirection.FORWARD;
446
+
447
+ // --- Path Constraints ---
448
+ PathConstraints.maxVelocity = 50;
449
+ PathConstraints.maxAcceleration = 50;
450
+ PathConstraints.maxAngularVelocity = Math.toRadians(300);
451
+ PathConstraints.maxAngularAcceleration = Math.toRadians(300);
452
+ }
453
+
454
+ /**
455
+ * Creates and returns a configured Follower instance.
456
+ * Call this from your OpMode's init() method.
457
+ *
458
+ * @param hardwareMap the hardware map from the OpMode
459
+ * @return a configured Follower
460
+ */
461
+ public static Follower createFollower(HardwareMap hardwareMap) {
462
+ return new Follower(hardwareMap);
463
+ }
464
+ }`,
465
+ "dashboard-config": `package org.firstinspires.ftc.teamcode;
466
+
467
+ import com.acmerobotics.dashboard.FtcDashboard;
468
+ import com.acmerobotics.dashboard.config.Config;
469
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
470
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
471
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
472
+
473
+ /**
474
+ * Demonstrates @Config usage with FTC Dashboard.
475
+ *
476
+ * CRITICAL: Always read static fields at the POINT OF USE so that
477
+ * dashboard edits take effect immediately. Never cache them in
478
+ * local variables during init().
479
+ */
480
+ @Config
481
+ @TeleOp(name = "Dashboard Config Demo", group = "Examples")
482
+ public class DashboardConfigExample extends OpMode {
483
+
484
+ // --- Primitive types: editable from the dashboard ---
485
+ public static double POWER = 0.5;
486
+ public static int TARGET_POSITION = 1000;
487
+ public static boolean USE_PID = true;
488
+ public static String ALLIANCE_COLOR = "RED";
489
+
490
+ // --- Enum: shows a dropdown in the dashboard ---
491
+ public enum DriveMode {
492
+ FIELD_CENTRIC,
493
+ ROBOT_CENTRIC,
494
+ TANK
495
+ }
496
+ public static DriveMode DRIVE_MODE = DriveMode.FIELD_CENTRIC;
497
+
498
+ // --- Nested object: each field editable in the dashboard ---
499
+ public static class PIDCoefficients {
500
+ public double kP = 0.05;
501
+ public double kI = 0.0;
502
+ public double kD = 0.01;
503
+ public double kF = 0.0;
504
+ }
505
+ public static PIDCoefficients pidCoeffs = new PIDCoefficients();
506
+
507
+ @Override
508
+ public void init() {
509
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
510
+ telemetry.addData("Status", "Initialized - edit values on dashboard!");
511
+ telemetry.update();
512
+ }
513
+
514
+ @Override
515
+ public void loop() {
516
+ // =====================================================
517
+ // CORRECT: Read statics at the point of use.
518
+ // Dashboard changes are reflected IMMEDIATELY each loop.
519
+ // =====================================================
520
+ double currentPower = POWER; // fresh every loop
521
+ int currentTarget = TARGET_POSITION; // fresh every loop
522
+ boolean usePid = USE_PID; // fresh every loop
523
+
524
+ // PID coefficients are also read fresh because pidCoeffs
525
+ // is a static reference and its fields are public.
526
+ double p = pidCoeffs.kP;
527
+ double i = pidCoeffs.kI;
528
+ double d = pidCoeffs.kD;
529
+ double f = pidCoeffs.kF;
530
+
531
+ // Use the values ...
532
+ telemetry.addData("--- Current Values ---", "");
533
+ telemetry.addData("Power", "%.2f", currentPower);
534
+ telemetry.addData("Target Position", currentTarget);
535
+ telemetry.addData("Use PID", usePid);
536
+ telemetry.addData("Alliance", ALLIANCE_COLOR);
537
+ telemetry.addData("Drive Mode", DRIVE_MODE);
538
+ telemetry.addData("--- PID ---", "");
539
+ telemetry.addData("kP", "%.4f", p);
540
+ telemetry.addData("kI", "%.4f", i);
541
+ telemetry.addData("kD", "%.4f", d);
542
+ telemetry.addData("kF", "%.4f", f);
543
+
544
+ // =====================================================
545
+ // WRONG: Do NOT cache statics in instance fields in init()!
546
+ //
547
+ // private double cachedPower;
548
+ //
549
+ // public void init() {
550
+ // cachedPower = POWER; // BUG: copies the value once!
551
+ // }
552
+ //
553
+ // public void loop() {
554
+ // // cachedPower never changes even if you edit POWER
555
+ // // on the dashboard, because Java copied the double
556
+ // // value, not a reference to the static field.
557
+ // motor.setPower(cachedPower); // stale!
558
+ // }
559
+ //
560
+ // This is a "copy semantics" bug. Primitives are copied
561
+ // by value, so the local variable is disconnected from
562
+ // the static field after assignment.
563
+ // =====================================================
564
+
565
+ telemetry.update();
566
+ }
567
+ }`,
568
+ "bulk-reads": `package org.firstinspires.ftc.teamcode;
569
+
570
+ import com.acmerobotics.dashboard.FtcDashboard;
571
+ import com.acmerobotics.dashboard.config.Config;
572
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
573
+ import com.qualcomm.hardware.lynx.LynxModule;
574
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
575
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
576
+ import com.qualcomm.robotcore.hardware.DcMotor;
577
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
578
+ import com.qualcomm.robotcore.hardware.Servo;
579
+ import com.qualcomm.robotcore.util.ElapsedTime;
580
+
581
+ import java.util.List;
582
+
583
+ /**
584
+ * Demonstrates optimised hardware reads using MANUAL bulk caching.
585
+ *
586
+ * Without bulk caching every call to getPosition(), getCurrent(), etc.
587
+ * triggers a separate USB transaction (~3 ms each). With MANUAL mode
588
+ * a single clearBulkCache() call at the top of the loop fetches ALL
589
+ * sensor data in one transaction, and subsequent reads come from cache.
590
+ */
591
+ @Config
592
+ @TeleOp(name = "Bulk Reads Demo", group = "Examples")
593
+ public class BulkReadsExample extends OpMode {
594
+
595
+ public static double SERVO_POS_A = 0.0;
596
+ public static double SERVO_POS_B = 1.0;
597
+
598
+ private List<LynxModule> allHubs;
599
+
600
+ private DcMotorEx frontLeft;
601
+ private DcMotorEx frontRight;
602
+ private DcMotorEx backLeft;
603
+ private DcMotorEx backRight;
604
+
605
+ private Servo clawServo;
606
+
607
+ private ElapsedTime loopTimer;
608
+ private double maxLoopTime = 0;
609
+
610
+ @Override
611
+ public void init() {
612
+ // --- Enable MANUAL bulk caching on every hub ---
613
+ allHubs = hardwareMap.getAll(LynxModule.class);
614
+ for (LynxModule hub : allHubs) {
615
+ hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
616
+ }
617
+
618
+ // --- Motors ---
619
+ frontLeft = hardwareMap.get(DcMotorEx.class, "frontLeft");
620
+ frontRight = hardwareMap.get(DcMotorEx.class, "frontRight");
621
+ backLeft = hardwareMap.get(DcMotorEx.class, "backLeft");
622
+ backRight = hardwareMap.get(DcMotorEx.class, "backRight");
623
+
624
+ frontLeft.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
625
+ frontRight.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
626
+ backLeft.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
627
+ backRight.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
628
+
629
+ // --- Servos ---
630
+ clawServo = hardwareMap.get(Servo.class, "claw");
631
+
632
+ // --- Telemetry ---
633
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
634
+ loopTimer = new ElapsedTime();
635
+
636
+ telemetry.addData("Status", "Initialized with MANUAL bulk reads");
637
+ telemetry.update();
638
+ }
639
+
640
+ @Override
641
+ public void loop() {
642
+ // *** Clear bulk cache ONCE at the top of each loop ***
643
+ for (LynxModule hub : allHubs) {
644
+ hub.clearBulkCache();
645
+ }
646
+
647
+ loopTimer.reset();
648
+
649
+ // --- Drive: simple tank drive for demo ---
650
+ double drive = -gamepad1.left_stick_y;
651
+ double turn = gamepad1.right_stick_x;
652
+
653
+ frontLeft.setPower(drive + turn);
654
+ backLeft.setPower(drive + turn);
655
+ frontRight.setPower(drive - turn);
656
+ backRight.setPower(drive - turn);
657
+
658
+ // --- Servo: toggle with bumpers ---
659
+ if (gamepad1.left_bumper) {
660
+ clawServo.setPosition(SERVO_POS_A);
661
+ } else if (gamepad1.right_bumper) {
662
+ clawServo.setPosition(SERVO_POS_B);
663
+ }
664
+
665
+ // --- Read encoder positions (all from cache — fast!) ---
666
+ int flPos = frontLeft.getCurrentPosition();
667
+ int frPos = frontRight.getCurrentPosition();
668
+ int blPos = backLeft.getCurrentPosition();
669
+ int brPos = backRight.getCurrentPosition();
670
+
671
+ double elapsed = loopTimer.milliseconds();
672
+ if (elapsed > maxLoopTime) {
673
+ maxLoopTime = elapsed;
674
+ }
675
+
676
+ // --- Telemetry ---
677
+ telemetry.addData("--- Encoders ---", "");
678
+ telemetry.addData("FL", flPos);
679
+ telemetry.addData("FR", frPos);
680
+ telemetry.addData("BL", blPos);
681
+ telemetry.addData("BR", brPos);
682
+ telemetry.addData("--- Servo ---", "");
683
+ telemetry.addData("Claw Position", "%.2f", clawServo.getPosition());
684
+ telemetry.addData("--- Performance ---", "");
685
+ telemetry.addData("Loop Time (ms)", "%.2f", elapsed);
686
+ telemetry.addData("Max Loop Time (ms)", "%.2f", maxLoopTime);
687
+ telemetry.update();
688
+ }
689
+ }`,
690
+ "subsystem": `package org.firstinspires.ftc.teamcode;
691
+
692
+ import com.acmerobotics.dashboard.config.Config;
693
+ import com.qualcomm.robotcore.hardware.DcMotor;
694
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
695
+ import com.qualcomm.robotcore.hardware.HardwareMap;
696
+ import com.qualcomm.robotcore.hardware.Servo;
697
+
698
+ import org.firstinspires.ftc.robotcore.external.Telemetry;
699
+
700
+ /**
701
+ * Example hardware subsystem for a linear-slide lift with a
702
+ * claw servo. Designed to be instantiated from an OpMode.
703
+ *
704
+ * Usage:
705
+ * LiftSubsystem lift = new LiftSubsystem(hardwareMap);
706
+ * lift.goToPosition(LiftSubsystem.HIGH_POSITION);
707
+ * lift.setClaw(LiftSubsystem.CLAW_OPEN);
708
+ * lift.update();
709
+ */
710
+ @Config
711
+ public class LiftSubsystem {
712
+
713
+ // --- Dashboard-tunable positions ---
714
+ // Always read these statics at the POINT OF USE so dashboard
715
+ // edits are reflected immediately.
716
+ public static int HIGH_POSITION = 2800;
717
+ public static int MID_POSITION = 1400;
718
+ public static int LOW_POSITION = 400;
719
+ public static int HOME_POSITION = 0;
720
+ public static int POSITION_TOLERANCE = 15;
721
+
722
+ public static double LIFT_POWER = 0.8;
723
+ public static double HOLD_POWER = 0.15;
724
+
725
+ public static double CLAW_OPEN = 0.6;
726
+ public static double CLAW_CLOSED = 0.15;
727
+
728
+ public static double kP = 0.005;
729
+ public static double kF = 0.1;
730
+
731
+ private final DcMotorEx liftMotor;
732
+ private final Servo clawServo;
733
+
734
+ private int targetPosition = 0;
735
+
736
+ /**
737
+ * Initializes the subsystem from the hardware map.
738
+ *
739
+ * @param hardwareMap the OpMode's hardware map
740
+ */
741
+ public LiftSubsystem(HardwareMap hardwareMap) {
742
+ liftMotor = hardwareMap.get(DcMotorEx.class, "liftMotor");
743
+ liftMotor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
744
+ liftMotor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
745
+ liftMotor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
746
+
747
+ clawServo = hardwareMap.get(Servo.class, "clawServo");
748
+ clawServo.setPosition(CLAW_CLOSED);
749
+ }
750
+
751
+ /**
752
+ * Sets the target encoder position for the lift.
753
+ * Use the public static constants (HIGH_POSITION, etc.) which
754
+ * are editable from the dashboard.
755
+ */
756
+ public void goToPosition(int position) {
757
+ targetPosition = position;
758
+ }
759
+
760
+ /**
761
+ * Software PID update — call this every loop iteration.
762
+ * Reads kP and kF from statics at point of use so dashboard
763
+ * changes take effect immediately.
764
+ */
765
+ public void update() {
766
+ int currentPosition = liftMotor.getCurrentPosition();
767
+ int error = targetPosition - currentPosition;
768
+
769
+ // Read tunable coefficients fresh each loop
770
+ double power = error * kP + Math.signum(error) * kF;
771
+ power = Math.max(-LIFT_POWER, Math.min(LIFT_POWER, power));
772
+
773
+ // Apply a small holding power when near target to resist gravity
774
+ if (Math.abs(error) < POSITION_TOLERANCE && targetPosition > 50) {
775
+ power = HOLD_POWER;
776
+ }
777
+
778
+ liftMotor.setPower(power);
779
+ }
780
+
781
+ /** Opens or closes the claw. */
782
+ public void setClaw(double position) {
783
+ clawServo.setPosition(position);
784
+ }
785
+
786
+ /** @return true if the lift is within tolerance of its target. */
787
+ public boolean isAtTarget() {
788
+ return Math.abs(liftMotor.getCurrentPosition() - targetPosition) < POSITION_TOLERANCE;
789
+ }
790
+
791
+ /** @return the current encoder position of the lift motor. */
792
+ public int getCurrentPosition() {
793
+ return liftMotor.getCurrentPosition();
794
+ }
795
+
796
+ /** @return the active target position. */
797
+ public int getTargetPosition() {
798
+ return targetPosition;
799
+ }
800
+
801
+ /** Sends subsystem telemetry to the driver station / dashboard. */
802
+ public void outputTelemetry(Telemetry telemetry) {
803
+ int currentPos = liftMotor.getCurrentPosition();
804
+ telemetry.addData("--- Lift ---", "");
805
+ telemetry.addData("Target", targetPosition);
806
+ telemetry.addData("Current", currentPos);
807
+ telemetry.addData("Error", targetPosition - currentPos);
808
+ telemetry.addData("At Target", isAtTarget());
809
+ telemetry.addData("Motor Power", "%.2f", liftMotor.getPower());
810
+ telemetry.addData("Claw Pos", "%.2f", clawServo.getPosition());
811
+ }
812
+ }`,
813
+ "pid-tuning": `package org.firstinspires.ftc.teamcode;
814
+
815
+ import com.acmerobotics.dashboard.FtcDashboard;
816
+ import com.acmerobotics.dashboard.config.Config;
817
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
818
+ import com.qualcomm.hardware.lynx.LynxModule;
819
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
820
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
821
+ import com.qualcomm.robotcore.hardware.DcMotor;
822
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
823
+ import com.qualcomm.robotcore.util.ElapsedTime;
824
+
825
+ import java.util.List;
826
+
827
+ /**
828
+ * PID tuning OpMode for a single motor with encoder feedback.
829
+ *
830
+ * Open the FTC Dashboard graph panel and add:
831
+ * - target
832
+ * - position
833
+ * - error
834
+ * - output
835
+ *
836
+ * Then adjust kP, kI, kD, kF live to tune the controller.
837
+ */
838
+ @Config
839
+ @TeleOp(name = "PID Tuner", group = "Tuning")
840
+ public class PIDTuningExample extends OpMode {
841
+
842
+ // --- Dashboard-tunable PID coefficients ---
843
+ public static double kP = 0.01;
844
+ public static double kI = 0.0;
845
+ public static double kD = 0.0;
846
+ public static double kF = 0.0;
847
+
848
+ // --- Dashboard-tunable target ---
849
+ public static int TARGET = 1000;
850
+ public static double MAX_OUTPUT = 0.8;
851
+
852
+ private DcMotorEx motor;
853
+
854
+ private double integralSum = 0;
855
+ private double lastError = 0;
856
+ private ElapsedTime timer;
857
+
858
+ private List<LynxModule> allHubs;
859
+
860
+ @Override
861
+ public void init() {
862
+ // Bulk reads for performance
863
+ allHubs = hardwareMap.getAll(LynxModule.class);
864
+ for (LynxModule hub : allHubs) {
865
+ hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
866
+ }
867
+
868
+ motor = hardwareMap.get(DcMotorEx.class, "liftMotor");
869
+ motor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
870
+ motor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
871
+ motor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
872
+
873
+ timer = new ElapsedTime();
874
+
875
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
876
+ telemetry.addData("Status", "PID Tuner ready — adjust kP/kI/kD/kF on dashboard");
877
+ telemetry.update();
878
+ }
879
+
880
+ @Override
881
+ public void start() {
882
+ timer.reset();
883
+ integralSum = 0;
884
+ lastError = 0;
885
+ }
886
+
887
+ @Override
888
+ public void loop() {
889
+ // Clear bulk cache
890
+ for (LynxModule hub : allHubs) {
891
+ hub.clearBulkCache();
892
+ }
893
+
894
+ // Read coefficients from dashboard statics at point of use
895
+ double p = kP;
896
+ double i = kI;
897
+ double d = kD;
898
+ double f = kF;
899
+ int target = TARGET;
900
+
901
+ int currentPosition = motor.getCurrentPosition();
902
+ double error = target - currentPosition;
903
+
904
+ double dt = timer.seconds();
905
+ timer.reset();
906
+
907
+ // Proportional
908
+ double pTerm = p * error;
909
+
910
+ // Integral (with windup guard)
911
+ integralSum += error * dt;
912
+ integralSum = Math.max(-1000, Math.min(1000, integralSum));
913
+ double iTerm = i * integralSum;
914
+
915
+ // Derivative
916
+ double derivative = (dt > 0) ? (error - lastError) / dt : 0;
917
+ double dTerm = d * derivative;
918
+ lastError = error;
919
+
920
+ // Feedforward
921
+ double fTerm = f * Math.signum(error);
922
+
923
+ // Total output, clamped
924
+ double output = pTerm + iTerm + dTerm + fTerm;
925
+ output = Math.max(-MAX_OUTPUT, Math.min(MAX_OUTPUT, output));
926
+
927
+ motor.setPower(output);
928
+
929
+ // Telemetry — these keys are graphable in the dashboard
930
+ telemetry.addData("target", target);
931
+ telemetry.addData("position", currentPosition);
932
+ telemetry.addData("error", error);
933
+ telemetry.addData("output", output);
934
+ telemetry.addData("--- PID Terms ---", "");
935
+ telemetry.addData("P term", "%.4f", pTerm);
936
+ telemetry.addData("I term", "%.4f", iTerm);
937
+ telemetry.addData("D term", "%.4f", dTerm);
938
+ telemetry.addData("F term", "%.4f", fTerm);
939
+ telemetry.addData("--- Coefficients ---", "");
940
+ telemetry.addData("kP", p);
941
+ telemetry.addData("kI", i);
942
+ telemetry.addData("kD", d);
943
+ telemetry.addData("kF", f);
944
+ telemetry.update();
945
+ }
946
+
947
+ @Override
948
+ public void stop() {
949
+ motor.setPower(0);
950
+ }
951
+ }`,
952
+ "vision-pipeline": `package org.firstinspires.ftc.teamcode;
953
+
954
+ import com.acmerobotics.dashboard.FtcDashboard;
955
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
956
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
957
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
958
+
959
+ import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
960
+ import org.firstinspires.ftc.vision.VisionPortal;
961
+ import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
962
+ import org.firstinspires.ftc.vision.apriltag.AprilTagProcessor;
963
+
964
+ import java.util.List;
965
+
966
+ /**
967
+ * Demonstrates VisionPortal with an AprilTag processor.
968
+ * Streams the camera feed to the FTC Dashboard for remote viewing.
969
+ */
970
+ @TeleOp(name = "Vision AprilTag Demo", group = "Vision")
971
+ public class VisionPipelineExample extends OpMode {
972
+
973
+ private AprilTagProcessor aprilTagProcessor;
974
+ private VisionPortal visionPortal;
975
+
976
+ @Override
977
+ public void init() {
978
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
979
+
980
+ // --- AprilTag Processor ---
981
+ aprilTagProcessor = new AprilTagProcessor.Builder()
982
+ .setDrawAxes(true)
983
+ .setDrawCubeProjection(true)
984
+ .setDrawTagOutline(true)
985
+ .build();
986
+
987
+ // --- Vision Portal ---
988
+ visionPortal = new VisionPortal.Builder()
989
+ .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
990
+ .addProcessor(aprilTagProcessor)
991
+ .enableLiveView(true)
992
+ .setStreamFormat(VisionPortal.StreamFormat.MJPEG)
993
+ .build();
994
+
995
+ // Stream camera to FTC Dashboard
996
+ FtcDashboard.getInstance().startCameraStream(visionPortal, 0);
997
+
998
+ telemetry.addData("Status", "VisionPortal initialized");
999
+ telemetry.addData("Camera State", visionPortal.getCameraState());
1000
+ telemetry.update();
1001
+ }
1002
+
1003
+ @Override
1004
+ public void init_loop() {
1005
+ telemetry.addData("Camera State", visionPortal.getCameraState());
1006
+
1007
+ List<AprilTagDetection> detections = aprilTagProcessor.getDetections();
1008
+ telemetry.addData("# Detections", detections.size());
1009
+ for (AprilTagDetection detection : detections) {
1010
+ telemetry.addLine(String.format("\\nTag ID %d (%s)", detection.id,
1011
+ detection.metadata != null ? detection.metadata.name : "Unknown"));
1012
+ }
1013
+ telemetry.update();
1014
+ }
1015
+
1016
+ @Override
1017
+ public void loop() {
1018
+ List<AprilTagDetection> detections = aprilTagProcessor.getDetections();
1019
+
1020
+ telemetry.addData("# AprilTags Detected", detections.size());
1021
+
1022
+ for (AprilTagDetection detection : detections) {
1023
+ if (detection.metadata != null) {
1024
+ telemetry.addLine(String.format("\\n==== Tag ID %d \\\"%s\\\" ====",
1025
+ detection.id, detection.metadata.name));
1026
+ } else {
1027
+ telemetry.addLine(String.format("\\n==== Tag ID %d (Unknown) ====",
1028
+ detection.id));
1029
+ }
1030
+
1031
+ telemetry.addLine(String.format("XYZ: %.1f, %.1f, %.1f (inches)",
1032
+ detection.ftcPose.x,
1033
+ detection.ftcPose.y,
1034
+ detection.ftcPose.z));
1035
+ telemetry.addLine(String.format("PRY: %.1f, %.1f, %.1f (degrees)",
1036
+ detection.ftcPose.pitch,
1037
+ detection.ftcPose.roll,
1038
+ detection.ftcPose.yaw));
1039
+ telemetry.addLine(String.format("Range: %.1f in", detection.ftcPose.range));
1040
+ telemetry.addLine(String.format("Bearing: %.1f deg", detection.ftcPose.bearing));
1041
+ telemetry.addLine(String.format("Elevation: %.1f deg", detection.ftcPose.elevation));
1042
+ }
1043
+
1044
+ if (detections.isEmpty()) {
1045
+ telemetry.addData("Info", "No AprilTags detected");
1046
+ }
1047
+
1048
+ telemetry.update();
1049
+ }
1050
+
1051
+ @Override
1052
+ public void stop() {
1053
+ if (visionPortal != null) {
1054
+ visionPortal.close();
1055
+ }
1056
+ }
1057
+ }`,
1058
+ "custom-pid-drive": `package org.firstinspires.ftc.teamcode;
1059
+
1060
+ import com.acmerobotics.dashboard.FtcDashboard;
1061
+ import com.acmerobotics.dashboard.config.Config;
1062
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
1063
+ import com.qualcomm.hardware.rev.RevHubOrientationOnRobot;
1064
+ import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
1065
+ import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
1066
+ import com.qualcomm.robotcore.hardware.DcMotor;
1067
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
1068
+ import com.qualcomm.robotcore.hardware.IMU;
1069
+ import com.qualcomm.robotcore.util.ElapsedTime;
1070
+ import com.qualcomm.robotcore.util.Range;
1071
+
1072
+ import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
1073
+
1074
+ /**
1075
+ * Encoder-based autonomous driving with PID heading correction.
1076
+ * No pathing library required — uses the built-in motor encoders
1077
+ * and IMU for straight-line driving.
1078
+ */
1079
+ @Config
1080
+ @Autonomous(name = "Custom PID Drive", group = "Auto")
1081
+ public class CustomPIDDriveExample extends LinearOpMode {
1082
+
1083
+ // --- Dashboard-tunable PID ---
1084
+ public static double HEADING_KP = 0.02;
1085
+ public static double HEADING_KI = 0.0;
1086
+ public static double HEADING_KD = 0.005;
1087
+
1088
+ // --- Dashboard-tunable drive parameters ---
1089
+ public static double DRIVE_SPEED = 0.4;
1090
+ public static double TURN_SPEED = 0.3;
1091
+ public static double COUNTS_PER_INCH = 537.7 / (4.0 * Math.PI); // goBILDA 312 RPM, 4" wheels
1092
+ public static double DRIVE_TIMEOUT = 5.0;
1093
+
1094
+ private DcMotorEx frontLeft, frontRight, backLeft, backRight;
1095
+ private IMU imu;
1096
+
1097
+ @Override
1098
+ public void runOpMode() {
1099
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
1100
+
1101
+ // --- Motor setup ---
1102
+ frontLeft = hardwareMap.get(DcMotorEx.class, "frontLeft");
1103
+ frontRight = hardwareMap.get(DcMotorEx.class, "frontRight");
1104
+ backLeft = hardwareMap.get(DcMotorEx.class, "backLeft");
1105
+ backRight = hardwareMap.get(DcMotorEx.class, "backRight");
1106
+
1107
+ frontLeft.setDirection(DcMotor.Direction.REVERSE);
1108
+ backLeft.setDirection(DcMotor.Direction.REVERSE);
1109
+ frontRight.setDirection(DcMotor.Direction.FORWARD);
1110
+ backRight.setDirection(DcMotor.Direction.FORWARD);
1111
+
1112
+ setMotorMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
1113
+ setMotorMode(DcMotor.RunMode.RUN_USING_ENCODER);
1114
+
1115
+ // --- IMU setup ---
1116
+ imu = hardwareMap.get(IMU.class, "imu");
1117
+ IMU.Parameters imuParams = new IMU.Parameters(
1118
+ new RevHubOrientationOnRobot(
1119
+ RevHubOrientationOnRobot.LogoFacingDirection.UP,
1120
+ RevHubOrientationOnRobot.UsbFacingDirection.FORWARD));
1121
+ imu.initialize(imuParams);
1122
+ imu.resetYaw();
1123
+
1124
+ telemetry.addData("Status", "Initialized");
1125
+ telemetry.update();
1126
+
1127
+ waitForStart();
1128
+
1129
+ // --- Autonomous routine ---
1130
+ encoderDrive(DRIVE_SPEED, 24, 24, DRIVE_TIMEOUT); // Forward 24 in
1131
+ encoderDrive(TURN_SPEED, 12, -12, DRIVE_TIMEOUT); // Turn right
1132
+ encoderDrive(DRIVE_SPEED, 18, 18, DRIVE_TIMEOUT); // Forward 18 in
1133
+ encoderDrive(DRIVE_SPEED, -12, -12, DRIVE_TIMEOUT); // Reverse 12 in
1134
+
1135
+ telemetry.addData("Path", "Complete");
1136
+ telemetry.update();
1137
+ sleep(1000);
1138
+ }
1139
+
1140
+ /**
1141
+ * Drive a specified distance using encoder counts with PID
1142
+ * heading correction from the IMU.
1143
+ *
1144
+ * @param speed max motor power (0..1)
1145
+ * @param leftInches distance for left side (negative = reverse)
1146
+ * @param rightInches distance for right side (negative = reverse)
1147
+ * @param timeout max seconds before aborting
1148
+ */
1149
+ public void encoderDrive(double speed, double leftInches, double rightInches, double timeout) {
1150
+ if (!opModeIsActive()) return;
1151
+
1152
+ // Read the dashboard-tunable constants fresh
1153
+ double countsPerInch = COUNTS_PER_INCH;
1154
+
1155
+ int leftTarget = frontLeft.getCurrentPosition() + (int)(leftInches * countsPerInch);
1156
+ int rightTarget = frontRight.getCurrentPosition() + (int)(rightInches * countsPerInch);
1157
+
1158
+ frontLeft.setTargetPosition(leftTarget);
1159
+ backLeft.setTargetPosition(leftTarget);
1160
+ frontRight.setTargetPosition(rightTarget);
1161
+ backRight.setTargetPosition(rightTarget);
1162
+
1163
+ setMotorMode(DcMotor.RunMode.RUN_TO_POSITION);
1164
+
1165
+ double targetHeading = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.DEGREES);
1166
+ double headingIntegral = 0;
1167
+ double lastHeadingError = 0;
1168
+
1169
+ ElapsedTime runtime = new ElapsedTime();
1170
+ ElapsedTime pidTimer = new ElapsedTime();
1171
+
1172
+ // Set initial power
1173
+ setAllMotorPower(Math.abs(speed));
1174
+
1175
+ while (opModeIsActive()
1176
+ && runtime.seconds() < timeout
1177
+ && (frontLeft.isBusy() || frontRight.isBusy())) {
1178
+
1179
+ double dt = pidTimer.seconds();
1180
+ pidTimer.reset();
1181
+
1182
+ // PID heading correction — read coefficients fresh each iteration
1183
+ double currentHeading = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.DEGREES);
1184
+ double headingError = targetHeading - currentHeading;
1185
+
1186
+ // Normalize to -180..180
1187
+ while (headingError > 180) headingError -= 360;
1188
+ while (headingError < -180) headingError += 360;
1189
+
1190
+ headingIntegral += headingError * dt;
1191
+ headingIntegral = Range.clip(headingIntegral, -50, 50);
1192
+
1193
+ double headingDerivative = (dt > 0) ? (headingError - lastHeadingError) / dt : 0;
1194
+ lastHeadingError = headingError;
1195
+
1196
+ double correction = (HEADING_KP * headingError)
1197
+ + (HEADING_KI * headingIntegral)
1198
+ + (HEADING_KD * headingDerivative);
1199
+
1200
+ double leftPower = Range.clip(speed + correction, -1.0, 1.0);
1201
+ double rightPower = Range.clip(speed - correction, -1.0, 1.0);
1202
+
1203
+ frontLeft.setPower(Math.abs(leftPower));
1204
+ backLeft.setPower(Math.abs(leftPower));
1205
+ frontRight.setPower(Math.abs(rightPower));
1206
+ backRight.setPower(Math.abs(rightPower));
1207
+
1208
+ telemetry.addData("Target L/R", "%7d / %7d", leftTarget, rightTarget);
1209
+ telemetry.addData("Current L/R", "%7d / %7d",
1210
+ frontLeft.getCurrentPosition(), frontRight.getCurrentPosition());
1211
+ telemetry.addData("Heading Target", "%.1f", targetHeading);
1212
+ telemetry.addData("Heading Current", "%.1f", currentHeading);
1213
+ telemetry.addData("Heading Error", "%.2f", headingError);
1214
+ telemetry.addData("Correction", "%.3f", correction);
1215
+ telemetry.addData("Power L/R", "%.2f / %.2f", leftPower, rightPower);
1216
+ telemetry.update();
1217
+ }
1218
+
1219
+ setAllMotorPower(0);
1220
+ setMotorMode(DcMotor.RunMode.RUN_USING_ENCODER);
1221
+ }
1222
+
1223
+ private void setMotorMode(DcMotor.RunMode mode) {
1224
+ frontLeft.setMode(mode);
1225
+ frontRight.setMode(mode);
1226
+ backLeft.setMode(mode);
1227
+ backRight.setMode(mode);
1228
+ }
1229
+
1230
+ private void setAllMotorPower(double power) {
1231
+ frontLeft.setPower(power);
1232
+ frontRight.setPower(power);
1233
+ backLeft.setPower(power);
1234
+ backRight.setPower(power);
1235
+ }
1236
+ }`,
1237
+ "field-centric-drive": `package org.firstinspires.ftc.teamcode;
1238
+
1239
+ import com.acmerobotics.dashboard.FtcDashboard;
1240
+ import com.acmerobotics.dashboard.config.Config;
1241
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
1242
+ import com.qualcomm.hardware.lynx.LynxModule;
1243
+ import com.qualcomm.hardware.rev.RevHubOrientationOnRobot;
1244
+ import com.qualcomm.robotcore.eventloop.opmode.OpMode;
1245
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
1246
+ import com.qualcomm.robotcore.hardware.DcMotor;
1247
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
1248
+ import com.qualcomm.robotcore.hardware.IMU;
1249
+ import com.qualcomm.robotcore.util.ElapsedTime;
1250
+
1251
+ import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit;
1252
+
1253
+ import java.util.List;
1254
+
1255
+ /**
1256
+ * Field-centric mecanum TeleOp with IMU heading reference.
1257
+ *
1258
+ * The left stick controls translation relative to the FIELD,
1259
+ * not the robot. The right stick controls rotation. Press
1260
+ * gamepad1.back to reset the heading reference so "forward"
1261
+ * always means toward the opposing alliance wall.
1262
+ */
1263
+ @Config
1264
+ @TeleOp(name = "Field-Centric Mecanum", group = "TeleOp")
1265
+ public class FieldCentricDriveExample extends OpMode {
1266
+
1267
+ public static double NORMAL_SPEED = 1.0;
1268
+ public static double SLOW_SPEED = 0.35;
1269
+
1270
+ private DcMotorEx frontLeft, frontRight, backLeft, backRight;
1271
+ private IMU imu;
1272
+
1273
+ private List<LynxModule> allHubs;
1274
+ private ElapsedTime loopTimer;
1275
+
1276
+ private boolean slowModeActive = false;
1277
+ private boolean previousAState = false;
1278
+
1279
+ @Override
1280
+ public void init() {
1281
+ // --- Manual bulk reads ---
1282
+ allHubs = hardwareMap.getAll(LynxModule.class);
1283
+ for (LynxModule hub : allHubs) {
1284
+ hub.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL);
1285
+ }
1286
+
1287
+ // --- Motors ---
1288
+ frontLeft = hardwareMap.get(DcMotorEx.class, "frontLeft");
1289
+ frontRight = hardwareMap.get(DcMotorEx.class, "frontRight");
1290
+ backLeft = hardwareMap.get(DcMotorEx.class, "backLeft");
1291
+ backRight = hardwareMap.get(DcMotorEx.class, "backRight");
1292
+
1293
+ frontLeft.setDirection(DcMotor.Direction.REVERSE);
1294
+ backLeft.setDirection(DcMotor.Direction.REVERSE);
1295
+ frontRight.setDirection(DcMotor.Direction.FORWARD);
1296
+ backRight.setDirection(DcMotor.Direction.FORWARD);
1297
+
1298
+ frontLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
1299
+ frontRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
1300
+ backLeft.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
1301
+ backRight.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
1302
+
1303
+ // --- IMU ---
1304
+ imu = hardwareMap.get(IMU.class, "imu");
1305
+ IMU.Parameters imuParams = new IMU.Parameters(
1306
+ new RevHubOrientationOnRobot(
1307
+ RevHubOrientationOnRobot.LogoFacingDirection.UP,
1308
+ RevHubOrientationOnRobot.UsbFacingDirection.FORWARD));
1309
+ imu.initialize(imuParams);
1310
+ imu.resetYaw();
1311
+
1312
+ // --- Telemetry ---
1313
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
1314
+ loopTimer = new ElapsedTime();
1315
+
1316
+ telemetry.addData("Status", "Initialized — press Back to reset heading");
1317
+ telemetry.update();
1318
+ }
1319
+
1320
+ @Override
1321
+ public void loop() {
1322
+ // Clear bulk cache once per loop
1323
+ for (LynxModule hub : allHubs) {
1324
+ hub.clearBulkCache();
1325
+ }
1326
+
1327
+ loopTimer.reset();
1328
+
1329
+ // --- Reset heading reference ---
1330
+ if (gamepad1.back) {
1331
+ imu.resetYaw();
1332
+ }
1333
+
1334
+ // --- Slow mode toggle (rising edge on A) ---
1335
+ boolean currentAState = gamepad1.a;
1336
+ if (currentAState && !previousAState) {
1337
+ slowModeActive = !slowModeActive;
1338
+ }
1339
+ previousAState = currentAState;
1340
+
1341
+ // Read speed from dashboard-tunable statics at point of use
1342
+ double speedMultiplier = slowModeActive ? SLOW_SPEED : NORMAL_SPEED;
1343
+
1344
+ // --- Read gamepad inputs ---
1345
+ double y = -gamepad1.left_stick_y; // forward is positive
1346
+ double x = gamepad1.left_stick_x; // right is positive
1347
+ double rx = gamepad1.right_stick_x; // clockwise is positive
1348
+
1349
+ // --- Get heading from IMU ---
1350
+ double heading = imu.getRobotYawPitchRollAngles().getYaw(AngleUnit.RADIANS);
1351
+
1352
+ // --- Field-centric rotation matrix ---
1353
+ double rotX = x * Math.cos(-heading) - y * Math.sin(-heading);
1354
+ double rotY = x * Math.sin(-heading) + y * Math.cos(-heading);
1355
+
1356
+ // Counteract imperfect strafing (optional 1.1 multiplier)
1357
+ rotX *= 1.1;
1358
+
1359
+ // --- Mecanum kinematics ---
1360
+ double denominator = Math.max(Math.abs(rotY) + Math.abs(rotX) + Math.abs(rx), 1.0);
1361
+ double flPower = (rotY + rotX + rx) / denominator;
1362
+ double blPower = (rotY - rotX + rx) / denominator;
1363
+ double frPower = (rotY - rotX - rx) / denominator;
1364
+ double brPower = (rotY + rotX - rx) / denominator;
1365
+
1366
+ // --- Apply speed multiplier and set power ---
1367
+ frontLeft.setPower(flPower * speedMultiplier);
1368
+ backLeft.setPower(blPower * speedMultiplier);
1369
+ frontRight.setPower(frPower * speedMultiplier);
1370
+ backRight.setPower(brPower * speedMultiplier);
1371
+
1372
+ // --- Telemetry ---
1373
+ double loopMs = loopTimer.milliseconds();
1374
+ telemetry.addData("--- Drive ---", "");
1375
+ telemetry.addData("Mode", "Field-Centric");
1376
+ telemetry.addData("Slow Mode", slowModeActive ? "ON" : "OFF");
1377
+ telemetry.addData("Speed", "%.2f", speedMultiplier);
1378
+ telemetry.addData("--- Heading ---", "");
1379
+ telemetry.addData("Heading (deg)", "%.1f", Math.toDegrees(heading));
1380
+ telemetry.addData("--- Motor Powers ---", "");
1381
+ telemetry.addData("FL / FR", "%.2f / %.2f", flPower * speedMultiplier, frPower * speedMultiplier);
1382
+ telemetry.addData("BL / BR", "%.2f / %.2f", blPower * speedMultiplier, brPower * speedMultiplier);
1383
+ telemetry.addData("--- Performance ---", "");
1384
+ telemetry.addData("Loop Time (ms)", "%.2f", loopMs);
1385
+ telemetry.update();
1386
+ }
1387
+ }`,
1388
+ "command-teleop": `package org.firstinspires.ftc.teamcode.opmode.teleop;
1389
+
1390
+ import com.acmerobotics.dashboard.FtcDashboard;
1391
+ import com.acmerobotics.dashboard.config.Config;
1392
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
1393
+ import com.seattlesolvers.solverslib.command.CommandOpMode;
1394
+ import com.seattlesolvers.solverslib.command.CommandScheduler;
1395
+ import com.seattlesolvers.solverslib.command.InstantCommand;
1396
+ import com.seattlesolvers.solverslib.command.RunCommand;
1397
+ import com.seattlesolvers.solverslib.command.SequentialCommandGroup;
1398
+ import com.seattlesolvers.solverslib.command.WaitCommand;
1399
+ import com.seattlesolvers.solverslib.gamepad.GamepadEx;
1400
+ import com.seattlesolvers.solverslib.gamepad.GamepadKeys;
1401
+ import com.qualcomm.hardware.lynx.LynxModule;
1402
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
1403
+
1404
+ import org.firstinspires.ftc.teamcode.commands.custom.DefaultDriveCommand;
1405
+ import org.firstinspires.ftc.teamcode.commands.custom.LiftToPositionCommand;
1406
+ import org.firstinspires.ftc.teamcode.commands.group.ScoreHighBasketCommand;
1407
+ import org.firstinspires.ftc.teamcode.subsystems.ClawSubsystem;
1408
+ import org.firstinspires.ftc.teamcode.subsystems.DriveSubsystem;
1409
+ import org.firstinspires.ftc.teamcode.subsystems.LiftSubsystem;
1410
+
1411
+ /**
1412
+ * Command-based TeleOp using SolversLib.
1413
+ *
1414
+ * Controls:
1415
+ * Driver (gamepad1):
1416
+ * Left stick = translate (field-centric or robot-centric)
1417
+ * Right stick X = rotate
1418
+ * A = toggle slow mode
1419
+ *
1420
+ * Operator (gamepad2):
1421
+ * A = toggle claw open/close
1422
+ * Y = score high basket macro (lift → open → lower)
1423
+ * DPAD_UP = lift to high basket position
1424
+ * DPAD_DN = lift to home position
1425
+ * DPAD_LT = lift to low basket position
1426
+ * LB = manual lift down (while held)
1427
+ * RB = manual lift up (while held)
1428
+ */
1429
+ @Config
1430
+ @TeleOp(name = "Command TeleOp", group = "Competition")
1431
+ public class CommandTeleOpExample extends CommandOpMode {
1432
+
1433
+ public static double SLOW_MULTIPLIER = 0.35;
1434
+ public static double NORMAL_MULTIPLIER = 1.0;
1435
+
1436
+ private boolean slowMode = false;
1437
+
1438
+ @Override
1439
+ public void initialize() {
1440
+ // --- Bulk reads via scheduler ---
1441
+ CommandScheduler.getInstance().setBulkCacheMode(LynxModule.BulkCachingMode.MANUAL);
1442
+
1443
+ // --- Telemetry to Dashboard ---
1444
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
1445
+
1446
+ // --- Gamepads ---
1447
+ GamepadEx driverGp = new GamepadEx(gamepad1);
1448
+ GamepadEx operatorGp = new GamepadEx(gamepad2);
1449
+
1450
+ // --- Subsystems ---
1451
+ DriveSubsystem drive = new DriveSubsystem(hardwareMap);
1452
+ LiftSubsystem lift = new LiftSubsystem(hardwareMap);
1453
+ ClawSubsystem claw = new ClawSubsystem(hardwareMap);
1454
+
1455
+ // --- Default Drive Command ---
1456
+ drive.setDefaultCommand(new DefaultDriveCommand(
1457
+ drive,
1458
+ () -> -driverGp.getLeftY() * (slowMode ? SLOW_MULTIPLIER : NORMAL_MULTIPLIER),
1459
+ () -> driverGp.getLeftX() * (slowMode ? SLOW_MULTIPLIER : NORMAL_MULTIPLIER),
1460
+ () -> driverGp.getRightX() * (slowMode ? SLOW_MULTIPLIER : NORMAL_MULTIPLIER)
1461
+ ));
1462
+
1463
+ // --- Driver Bindings ---
1464
+ driverGp.getGamepadButton(GamepadKeys.Button.A)
1465
+ .whenPressed(new InstantCommand(() -> slowMode = !slowMode));
1466
+
1467
+ // --- Operator Bindings ---
1468
+
1469
+ // Claw toggle
1470
+ operatorGp.getGamepadButton(GamepadKeys.Button.A)
1471
+ .whenPressed(new InstantCommand(claw::toggle, claw));
1472
+
1473
+ // Score macro: lift high → pause → open claw → pause → lower
1474
+ operatorGp.getGamepadButton(GamepadKeys.Button.Y)
1475
+ .whenPressed(new ScoreHighBasketCommand(lift, claw));
1476
+
1477
+ // Lift presets
1478
+ operatorGp.getGamepadButton(GamepadKeys.Button.DPAD_UP)
1479
+ .whenPressed(new LiftToPositionCommand(lift, LiftSubsystem.HIGH_BASKET));
1480
+
1481
+ operatorGp.getGamepadButton(GamepadKeys.Button.DPAD_DOWN)
1482
+ .whenPressed(new LiftToPositionCommand(lift, LiftSubsystem.HOME));
1483
+
1484
+ operatorGp.getGamepadButton(GamepadKeys.Button.DPAD_LEFT)
1485
+ .whenPressed(new LiftToPositionCommand(lift, LiftSubsystem.LOW_BASKET));
1486
+
1487
+ // Manual lift control (while held)
1488
+ operatorGp.getGamepadButton(GamepadKeys.Button.RIGHT_BUMPER)
1489
+ .whileHeld(new RunCommand(() -> lift.setManualPower(0.6), lift))
1490
+ .whenReleased(new InstantCommand(lift::exitManual, lift));
1491
+
1492
+ operatorGp.getGamepadButton(GamepadKeys.Button.LEFT_BUMPER)
1493
+ .whileHeld(new RunCommand(() -> lift.setManualPower(-0.4), lift))
1494
+ .whenReleased(new InstantCommand(lift::exitManual, lift));
1495
+
1496
+ telemetry.addData("Status", "Command TeleOp Initialized");
1497
+ telemetry.update();
1498
+ }
1499
+
1500
+ @Override
1501
+ public void run() {
1502
+ super.run(); // MUST call super to tick the scheduler
1503
+
1504
+ telemetry.addData("Slow Mode", slowMode ? "ON" : "OFF");
1505
+ telemetry.update();
1506
+ }
1507
+ }`,
1508
+ "command-auto": `package org.firstinspires.ftc.teamcode.opmode.auton;
1509
+
1510
+ import com.acmerobotics.dashboard.FtcDashboard;
1511
+ import com.acmerobotics.dashboard.config.Config;
1512
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
1513
+ import com.pedropathing.follower.Follower;
1514
+ import com.pedropathing.localization.Pose;
1515
+ import com.pedropathing.pathgen.BezierLine;
1516
+ import com.pedropathing.pathgen.PathChain;
1517
+ import com.pedropathing.pathgen.Point;
1518
+ import com.seattlesolvers.solverslib.command.CommandOpMode;
1519
+ import com.seattlesolvers.solverslib.command.CommandScheduler;
1520
+ import com.seattlesolvers.solverslib.command.InstantCommand;
1521
+ import com.seattlesolvers.solverslib.command.SequentialCommandGroup;
1522
+ import com.seattlesolvers.solverslib.command.WaitCommand;
1523
+ import com.seattlesolvers.solverslib.command.WaitUntilCommand;
1524
+ import com.qualcomm.hardware.lynx.LynxModule;
1525
+ import com.qualcomm.robotcore.eventloop.opmode.Autonomous;
1526
+
1527
+ import org.firstinspires.ftc.teamcode.commands.custom.FollowPathCommand;
1528
+ import org.firstinspires.ftc.teamcode.commands.custom.LiftToPositionCommand;
1529
+ import org.firstinspires.ftc.teamcode.subsystems.ClawSubsystem;
1530
+ import org.firstinspires.ftc.teamcode.subsystems.LiftSubsystem;
1531
+
1532
+ /**
1533
+ * Command-based autonomous using SolversLib + Pedro Pathing.
1534
+ *
1535
+ * This replaces the traditional FSM/switch-case pattern with a
1536
+ * declarative SequentialCommandGroup. Each step is a command,
1537
+ * and parallel actions use .alongWith().
1538
+ */
1539
+ @Config
1540
+ @Autonomous(name = "Command Auto - Left", group = "Auto")
1541
+ public class CommandAutoExample extends CommandOpMode {
1542
+
1543
+ // Dashboard-tunable poses
1544
+ public static double START_X = 9.0, START_Y = 60.0, START_HEADING = 0;
1545
+ public static double SCORE_X = 38.0, SCORE_Y = 68.0;
1546
+ public static double SAMPLE1_X = 37.0, SAMPLE1_Y = 30.0;
1547
+ public static double PARK_X = 10.0, PARK_Y = 10.0;
1548
+
1549
+ private Follower follower;
1550
+ private LiftSubsystem lift;
1551
+ private ClawSubsystem claw;
1552
+
1553
+ @Override
1554
+ public void initialize() {
1555
+ // Bulk reads
1556
+ CommandScheduler.getInstance().setBulkCacheMode(LynxModule.BulkCachingMode.MANUAL);
1557
+
1558
+ // Telemetry
1559
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
1560
+
1561
+ // Subsystems
1562
+ follower = new Follower(hardwareMap);
1563
+ lift = new LiftSubsystem(hardwareMap);
1564
+ claw = new ClawSubsystem(hardwareMap);
1565
+
1566
+ // Starting pose
1567
+ Pose startPose = new Pose(START_X, START_Y, Math.toRadians(START_HEADING));
1568
+ follower.setStartingPose(startPose);
1569
+
1570
+ // Build paths
1571
+ PathChain scorePreload = follower.pathBuilder()
1572
+ .addPath(new BezierLine(
1573
+ new Point(START_X, START_Y, Point.CARTESIAN),
1574
+ new Point(SCORE_X, SCORE_Y, Point.CARTESIAN)))
1575
+ .setLinearHeadingInterpolation(Math.toRadians(START_HEADING), 0)
1576
+ .build();
1577
+
1578
+ PathChain grabSample1 = follower.pathBuilder()
1579
+ .addPath(new BezierLine(
1580
+ new Point(SCORE_X, SCORE_Y, Point.CARTESIAN),
1581
+ new Point(SAMPLE1_X, SAMPLE1_Y, Point.CARTESIAN)))
1582
+ .setLinearHeadingInterpolation(0, 0)
1583
+ .build();
1584
+
1585
+ PathChain scoreSample1 = follower.pathBuilder()
1586
+ .addPath(new BezierLine(
1587
+ new Point(SAMPLE1_X, SAMPLE1_Y, Point.CARTESIAN),
1588
+ new Point(SCORE_X, SCORE_Y, Point.CARTESIAN)))
1589
+ .setLinearHeadingInterpolation(0, 0)
1590
+ .build();
1591
+
1592
+ PathChain park = follower.pathBuilder()
1593
+ .addPath(new BezierLine(
1594
+ new Point(SCORE_X, SCORE_Y, Point.CARTESIAN),
1595
+ new Point(PARK_X, PARK_Y, Point.CARTESIAN)))
1596
+ .setLinearHeadingInterpolation(0, 0)
1597
+ .build();
1598
+
1599
+ // Schedule the full autonomous as one command group
1600
+ schedule(new SequentialCommandGroup(
1601
+ // === Score preloaded sample ===
1602
+ // Drive to scoring position WHILE raising the lift
1603
+ new FollowPathCommand(follower, scorePreload, true)
1604
+ .alongWith(new LiftToPositionCommand(lift, LiftSubsystem.HIGH_BASKET)),
1605
+ // Wait for lift to finish reaching target
1606
+ new WaitUntilCommand(lift::atTarget),
1607
+ // Open claw to release
1608
+ new InstantCommand(claw::open, claw),
1609
+ new WaitCommand(300),
1610
+
1611
+ // === Grab sample 1 ===
1612
+ // Drive to sample WHILE lowering lift to intake height
1613
+ new FollowPathCommand(follower, grabSample1, true)
1614
+ .alongWith(new LiftToPositionCommand(lift, LiftSubsystem.INTAKE)),
1615
+ // Close claw to grab
1616
+ new InstantCommand(claw::close, claw),
1617
+ new WaitCommand(200),
1618
+
1619
+ // === Score sample 1 ===
1620
+ new FollowPathCommand(follower, scoreSample1, true)
1621
+ .alongWith(new LiftToPositionCommand(lift, LiftSubsystem.HIGH_BASKET)),
1622
+ new WaitUntilCommand(lift::atTarget),
1623
+ new InstantCommand(claw::open, claw),
1624
+ new WaitCommand(300),
1625
+
1626
+ // === Park ===
1627
+ new FollowPathCommand(follower, park, false)
1628
+ .alongWith(new LiftToPositionCommand(lift, LiftSubsystem.HOME))
1629
+ ));
1630
+
1631
+ telemetry.addData("Status", "Command Auto Initialized");
1632
+ telemetry.update();
1633
+ }
1634
+
1635
+ @Override
1636
+ public void run() {
1637
+ super.run();
1638
+
1639
+ // Telemetry
1640
+ Pose pose = follower.getPose();
1641
+ telemetry.addData("X", "%.1f", pose.getX());
1642
+ telemetry.addData("Y", "%.1f", pose.getY());
1643
+ telemetry.addData("Heading", "%.1f", Math.toDegrees(pose.getHeading()));
1644
+ telemetry.addData("Lift Pos", lift.getPosition());
1645
+ telemetry.addData("Lift Target", lift.getTarget());
1646
+ telemetry.update();
1647
+ }
1648
+ }`,
1649
+ "command-subsystem": `package org.firstinspires.ftc.teamcode.subsystems;
1650
+
1651
+ import com.acmerobotics.dashboard.config.Config;
1652
+ import com.seattlesolvers.solverslib.command.SubsystemBase;
1653
+ import com.qualcomm.robotcore.hardware.DcMotor;
1654
+ import com.qualcomm.robotcore.hardware.DcMotorEx;
1655
+ import com.qualcomm.robotcore.hardware.HardwareMap;
1656
+ import com.qualcomm.robotcore.util.ElapsedTime;
1657
+
1658
+ /**
1659
+ * Command-based lift subsystem using SolversLib's SubsystemBase.
1660
+ *
1661
+ * PID control runs continuously in periodic() — commands just set the target.
1662
+ * All tunable constants are @Config public static for FTC Dashboard.
1663
+ *
1664
+ * Usage:
1665
+ * LiftSubsystem lift = new LiftSubsystem(hardwareMap);
1666
+ * // Register is called in constructor
1667
+ *
1668
+ * // In a command:
1669
+ * lift.setTarget(LiftSubsystem.HIGH_BASKET);
1670
+ * // periodic() handles the PID automatically each scheduler tick
1671
+ */
1672
+ @Config
1673
+ public class LiftSubsystem extends SubsystemBase {
1674
+
1675
+ // --- Dashboard-tunable PID coefficients ---
1676
+ public static double kP = 0.005;
1677
+ public static double kI = 0.0;
1678
+ public static double kD = 0.001;
1679
+ public static double kF = 0.12; // gravity feedforward for linear slide
1680
+
1681
+ // --- Dashboard-tunable position presets ---
1682
+ public static int HOME = 0;
1683
+ public static int INTAKE = 50;
1684
+ public static int LOW_BASKET = 1200;
1685
+ public static int HIGH_BASKET = 2600;
1686
+ public static int HIGH_CHAMBER = 1800;
1687
+ public static int TOLERANCE = 15;
1688
+
1689
+ public static double MAX_POWER = 1.0;
1690
+
1691
+ // --- Hardware ---
1692
+ private final DcMotorEx motor;
1693
+
1694
+ // --- State ---
1695
+ private int targetPosition = 0;
1696
+ private boolean manualMode = false;
1697
+ private double manualPower = 0;
1698
+
1699
+ // --- PID state ---
1700
+ private double integralSum = 0;
1701
+ private double lastError = 0;
1702
+ private final ElapsedTime timer = new ElapsedTime();
1703
+
1704
+ public LiftSubsystem(HardwareMap hardwareMap) {
1705
+ motor = hardwareMap.get(DcMotorEx.class, "liftMotor");
1706
+ motor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
1707
+ motor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
1708
+ motor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
1709
+
1710
+ // Register with the CommandScheduler automatically
1711
+ register();
1712
+ }
1713
+
1714
+ /**
1715
+ * Called EVERY scheduler loop — runs PID regardless of which command is active.
1716
+ * This ensures the mechanism is always actively controlled.
1717
+ */
1718
+ @Override
1719
+ public void periodic() {
1720
+ if (manualMode) {
1721
+ motor.setPower(manualPower);
1722
+ // Track position so we can hold when manual mode exits
1723
+ targetPosition = motor.getCurrentPosition();
1724
+ return;
1725
+ }
1726
+
1727
+ // --- Software PID ---
1728
+ // Read @Config coefficients fresh each loop for live Dashboard tuning
1729
+ int current = motor.getCurrentPosition();
1730
+ double error = targetPosition - current;
1731
+ double dt = timer.seconds();
1732
+ timer.reset();
1733
+
1734
+ // Proportional
1735
+ double pTerm = kP * error;
1736
+
1737
+ // Integral (with anti-windup clamping)
1738
+ integralSum += error * dt;
1739
+ integralSum = Math.max(-500, Math.min(500, integralSum));
1740
+ double iTerm = kI * integralSum;
1741
+
1742
+ // Derivative
1743
+ double dTerm = (dt > 0) ? kD * (error - lastError) / dt : 0;
1744
+ lastError = error;
1745
+
1746
+ // Feedforward (constant gravity compensation for linear slides)
1747
+ double fTerm = (targetPosition > 50) ? kF : 0;
1748
+
1749
+ // Total output, clamped
1750
+ double output = pTerm + iTerm + dTerm + fTerm;
1751
+ output = Math.max(-MAX_POWER, Math.min(MAX_POWER, output));
1752
+ motor.setPower(output);
1753
+ }
1754
+
1755
+ // --- Target setters (called by commands) ---
1756
+
1757
+ public void setTarget(int position) {
1758
+ manualMode = false;
1759
+ targetPosition = position;
1760
+ integralSum = 0; // reset integral on new target to prevent windup
1761
+ }
1762
+
1763
+ public void goHome() { setTarget(HOME); }
1764
+ public void goIntake() { setTarget(INTAKE); }
1765
+ public void goLowBasket() { setTarget(LOW_BASKET); }
1766
+ public void goHighBasket() { setTarget(HIGH_BASKET); }
1767
+
1768
+ // --- Manual control (for joystick override) ---
1769
+
1770
+ public void setManualPower(double power) {
1771
+ manualMode = true;
1772
+ manualPower = power;
1773
+ }
1774
+
1775
+ public void exitManual() {
1776
+ manualMode = false;
1777
+ // Hold current position when switching back to PID
1778
+ targetPosition = motor.getCurrentPosition();
1779
+ integralSum = 0;
1780
+ }
1781
+
1782
+ // --- Queries ---
1783
+
1784
+ public boolean atTarget() {
1785
+ return !manualMode && Math.abs(motor.getCurrentPosition() - targetPosition) < TOLERANCE;
1786
+ }
1787
+
1788
+ public int getPosition() {
1789
+ return motor.getCurrentPosition();
1790
+ }
1791
+
1792
+ public int getTarget() {
1793
+ return targetPosition;
1794
+ }
1795
+
1796
+ public boolean isManual() {
1797
+ return manualMode;
1798
+ }
1799
+ }`
1800
+ };