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,843 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_KNOWLEDGE = void 0;
4
+ exports.DASHBOARD_KNOWLEDGE = {
5
+ configPattern: `
6
+ ## @Config Annotation Pattern (FTC Dashboard)
7
+
8
+ ### Import
9
+ \`\`\`java
10
+ import com.acmerobotics.dashboard.config.Config;
11
+ \`\`\`
12
+
13
+ ### Requirements
14
+ - The class MUST be annotated with \`@Config\`
15
+ - Fields MUST be \`public static\`
16
+ - Fields must NOT be \`final\`
17
+
18
+ ### Basic Usage
19
+ \`\`\`java
20
+ @Config
21
+ public class RobotConstants {
22
+ public static double DRIVE_SPEED = 0.8;
23
+ public static int ENCODER_TICKS = 1120;
24
+ public static boolean USE_GYRO = true;
25
+ }
26
+ \`\`\`
27
+
28
+ ### Custom Dashboard Group Name
29
+ \`\`\`java
30
+ @Config("Arm Settings")
31
+ public class ArmConstants {
32
+ public static double ARM_POWER = 0.5;
33
+ public static int ARM_TARGET = 300;
34
+ }
35
+ \`\`\`
36
+ By default the group name is the class simple name. \`@Config("CustomName")\` overrides this.
37
+
38
+ ### Disabling a Config Class
39
+ Add \`@Disabled\` to prevent it from appearing on the dashboard:
40
+ \`\`\`java
41
+ import com.qualcomm.robotcore.eventloop.opmode.Disabled;
42
+
43
+ @Config
44
+ @Disabled
45
+ public class DeprecatedConstants {
46
+ public static double OLD_VALUE = 1.0;
47
+ }
48
+ \`\`\`
49
+
50
+ ### Supported Types
51
+ - Primitives: \`boolean\`, \`int\`, \`long\`, \`float\`, \`double\`
52
+ - \`String\`
53
+ - \`enum\` types
54
+ - Custom objects (recursively expands all \`public\` non-\`static\` non-\`final\` instance fields)
55
+ - Arrays of supported types
56
+
57
+ ### Custom Object Example
58
+ \`\`\`java
59
+ @Config
60
+ public class PIDConstants {
61
+ public static class PIDCoefficients {
62
+ public double kP;
63
+ public double kI;
64
+ public double kD;
65
+
66
+ public PIDCoefficients(double kP, double kI, double kD) {
67
+ this.kP = kP;
68
+ this.kI = kI;
69
+ this.kD = kD;
70
+ }
71
+ }
72
+
73
+ public static PIDCoefficients DRIVE_PID = new PIDCoefficients(0.05, 0.0, 0.01);
74
+ public static PIDCoefficients ARM_PID = new PIDCoefficients(0.1, 0.0, 0.02);
75
+ }
76
+ \`\`\`
77
+ On the dashboard, \`DRIVE_PID\` expands into editable fields for \`kP\`, \`kI\`, and \`kD\`.
78
+
79
+ ### Kotlin Usage
80
+ In Kotlin, fields must use \`@JvmField var\` because the dashboard reads/writes fields directly via reflection — Kotlin properties with getters/setters are not supported.
81
+ \`\`\`kotlin
82
+ @Config
83
+ object RobotConstants {
84
+ @JvmField var DRIVE_SPEED = 0.8
85
+ @JvmField var USE_GYRO = true
86
+ @JvmField var ENCODER_TICKS = 1120
87
+ }
88
+ \`\`\`
89
+
90
+ ### Thread Safety
91
+ For \`long\` and \`double\` fields that are read in a loop thread and written from the dashboard, mark them \`volatile\` to guarantee visibility across threads:
92
+ \`\`\`java
93
+ @Config
94
+ public class TuningConstants {
95
+ public static volatile double HEADING_KP = 1.0;
96
+ public static volatile long DELAY_MS = 250;
97
+ public static int TICKS = 100; // int/boolean are already atomic, volatile optional
98
+ }
99
+ \`\`\`
100
+
101
+ ### CRITICAL: Copy Semantics Pitfall
102
+ Dashboard modifies the static field at runtime. If you copy the value into a local variable or constructor parameter, you will never see updates.
103
+
104
+ **WRONG** — caching the value into a constructor parameter:
105
+ \`\`\`java
106
+ @Config
107
+ public class DriveConstants {
108
+ public static double MAX_SPEED = 0.8;
109
+ }
110
+
111
+ public class MecanumDrive {
112
+ private final double maxSpeed;
113
+
114
+ public MecanumDrive(HardwareMap hardwareMap) {
115
+ // BUG: maxSpeed is copied once; dashboard changes to MAX_SPEED are never seen
116
+ this.maxSpeed = DriveConstants.MAX_SPEED;
117
+ }
118
+
119
+ public void drive(double forward, double strafe, double turn) {
120
+ // This always uses the stale value captured in the constructor
121
+ double speed = forward * maxSpeed;
122
+ // ...
123
+ }
124
+ }
125
+ \`\`\`
126
+
127
+ **RIGHT** — reading the static field at point of use:
128
+ \`\`\`java
129
+ public class MecanumDrive {
130
+ public MecanumDrive(HardwareMap hardwareMap) {
131
+ // No cached copy
132
+ }
133
+
134
+ public void drive(double forward, double strafe, double turn) {
135
+ // Reads the live static field every loop iteration — dashboard changes take effect immediately
136
+ double speed = forward * DriveConstants.MAX_SPEED;
137
+ // ...
138
+ }
139
+ }
140
+ \`\`\`
141
+
142
+ ### ValueProvider<T> — Programmatic Config
143
+ For variables that cannot be simple static fields, use the ValueProvider interface:
144
+ \`\`\`java
145
+ import com.acmerobotics.dashboard.FtcDashboard;
146
+ import com.acmerobotics.dashboard.config.ValueProvider;
147
+
148
+ FtcDashboard dashboard = FtcDashboard.getInstance();
149
+
150
+ dashboard.addConfigVariable("Motors", "leftPower", new ValueProvider<Double>() {
151
+ @Override
152
+ public Double get() {
153
+ return leftMotor.getPower();
154
+ }
155
+
156
+ @Override
157
+ public void set(Double value) {
158
+ leftMotor.setPower(value);
159
+ }
160
+ });
161
+
162
+ // Remove when no longer needed
163
+ dashboard.removeConfigVariable("Motors", "leftPower");
164
+ \`\`\`
165
+ `,
166
+ telemetry: `
167
+ ## FTC Dashboard Telemetry
168
+
169
+ ### Getting the Dashboard Instance
170
+ \`\`\`java
171
+ import com.acmerobotics.dashboard.FtcDashboard;
172
+
173
+ FtcDashboard dashboard = FtcDashboard.getInstance();
174
+ \`\`\`
175
+ \`getInstance()\` returns the singleton. It is available during \`init()\`, \`start()\`, and \`loop()\` (or their LinearOpMode equivalents).
176
+
177
+ ### Approach 1: MultipleTelemetry (Recommended Standard Pattern)
178
+ Sends telemetry to BOTH the Driver Station AND the dashboard simultaneously. This is THE standard pattern used by most FTC teams.
179
+ \`\`\`java
180
+ import com.acmerobotics.dashboard.FtcDashboard;
181
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
182
+
183
+ @TeleOp(name = "TeleOp Example")
184
+ public class MyTeleOp extends LinearOpMode {
185
+ @Override
186
+ public void runOpMode() {
187
+ // Wrap both telemetry objects — all calls go to DS + dashboard
188
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
189
+
190
+ waitForStart();
191
+
192
+ while (opModeIsActive()) {
193
+ telemetry.addData("Loop Time", "%.1f ms", getRuntime() * 1000);
194
+ telemetry.addData("Heading", "%.2f deg", getHeading());
195
+ telemetry.update(); // Sends to both DS and dashboard
196
+ }
197
+ }
198
+ }
199
+ \`\`\`
200
+
201
+ ### dashboard.getTelemetry()
202
+ \`\`\`java
203
+ Telemetry dashTelemetry = dashboard.getTelemetry();
204
+ dashTelemetry.addData("key", value);
205
+ dashTelemetry.update(); // Sends only to dashboard
206
+ \`\`\`
207
+ Returns a standard \`Telemetry\` adapter that speaks the dashboard protocol. Use this standalone if you only want dashboard output, or wrap it with \`MultipleTelemetry\`.
208
+
209
+ ### Approach 2: TelemetryPacket (Full Control)
210
+ For field overlay drawing and advanced telemetry, use packets directly.
211
+ \`\`\`java
212
+ import com.acmerobotics.dashboard.FtcDashboard;
213
+ import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
214
+
215
+ @TeleOp(name = "Packet Example")
216
+ public class PacketExample extends LinearOpMode {
217
+ @Override
218
+ public void runOpMode() {
219
+ FtcDashboard dashboard = FtcDashboard.getInstance();
220
+
221
+ waitForStart();
222
+
223
+ while (opModeIsActive()) {
224
+ TelemetryPacket packet = new TelemetryPacket();
225
+
226
+ // Key-value data (appears in telemetry panel)
227
+ packet.put("x", robotX);
228
+ packet.put("y", robotY);
229
+ packet.put("heading", Math.toDegrees(robotHeading));
230
+
231
+ // Free-form log lines
232
+ packet.addLine("Status: Running");
233
+ packet.addLine("Loop #" + loopCount);
234
+
235
+ // Field overlay drawing (see Canvas section)
236
+ packet.fieldOverlay()
237
+ .setFill("blue")
238
+ .fillCircle(robotX, robotY, 9);
239
+
240
+ dashboard.sendTelemetryPacket(packet);
241
+ }
242
+ }
243
+ }
244
+ \`\`\`
245
+
246
+ ### TelemetryPacket API
247
+ | Method | Description |
248
+ |--------|-------------|
249
+ | \`put(String key, Object value)\` | Add a key-value pair to the telemetry data |
250
+ | \`addLine(String text)\` | Add a free-form log line |
251
+ | \`fieldOverlay()\` | Returns the \`Canvas\` for drawing on the field overlay |
252
+ | \`new TelemetryPacket(false)\` | Create a packet that suppresses the default field image background |
253
+
254
+ ### Sending and Timing
255
+ \`\`\`java
256
+ dashboard.sendTelemetryPacket(packet); // Send one packet
257
+ dashboard.setTelemetryTransmissionInterval(50); // Set interval in ms (default: 100ms)
258
+ dashboard.clearTelemetry(); // Clear all telemetry on the dashboard
259
+ \`\`\`
260
+ - Default transmission interval: **100 ms**
261
+ - Buffer capacity: **100 packets** — if you produce packets faster than they are consumed, the oldest are dropped.
262
+
263
+ ### GOTCHA: Double-Packet Problem
264
+ If you use MultipleTelemetry AND manually call \`sendTelemetryPacket()\`, each loop iteration sends TWO packets — one from \`telemetry.update()\` (via MultipleTelemetry) and one from \`sendTelemetryPacket()\`. This wastes bandwidth and can cause confusing graph behavior.
265
+
266
+ **Pick ONE approach:**
267
+ - **MultipleTelemetry** for simple key-value telemetry to both DS and dashboard.
268
+ - **TelemetryPacket + sendTelemetryPacket()** when you need field overlay drawing or full packet control. If you also need DS telemetry, call \`telemetry.addData()\`/\`telemetry.update()\` on the original (non-wrapped) telemetry separately.
269
+
270
+ ### Complete Working Example — Both Approaches Combined Correctly
271
+ \`\`\`java
272
+ import com.acmerobotics.dashboard.FtcDashboard;
273
+ import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
274
+ import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
275
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
276
+
277
+ @TeleOp(name = "Dashboard Demo")
278
+ public class DashboardDemo extends LinearOpMode {
279
+ @Override
280
+ public void runOpMode() {
281
+ FtcDashboard dashboard = FtcDashboard.getInstance();
282
+
283
+ double robotX = 0, robotY = 0, robotHeading = 0;
284
+
285
+ waitForStart();
286
+
287
+ while (opModeIsActive()) {
288
+ // Update robot state (placeholder logic)
289
+ robotX += gamepad1.left_stick_x * 0.5;
290
+ robotY -= gamepad1.left_stick_y * 0.5;
291
+ robotHeading += gamepad1.right_stick_x * 0.05;
292
+
293
+ // --- Dashboard telemetry via TelemetryPacket (with field overlay) ---
294
+ TelemetryPacket packet = new TelemetryPacket();
295
+ packet.put("x", robotX);
296
+ packet.put("y", robotY);
297
+ packet.put("heading (deg)", Math.toDegrees(robotHeading));
298
+
299
+ packet.fieldOverlay()
300
+ .setStroke("blue")
301
+ .strokeCircle(robotX, robotY, 9)
302
+ .setStroke("green")
303
+ .strokeLine(
304
+ robotX, robotY,
305
+ robotX + 12 * Math.cos(robotHeading),
306
+ robotY + 12 * Math.sin(robotHeading)
307
+ );
308
+
309
+ dashboard.sendTelemetryPacket(packet);
310
+
311
+ // --- Driver Station telemetry (separate, no MultipleTelemetry wrapper) ---
312
+ telemetry.addData("X", "%.1f", robotX);
313
+ telemetry.addData("Y", "%.1f", robotY);
314
+ telemetry.addData("Heading", "%.1f deg", Math.toDegrees(robotHeading));
315
+ telemetry.update();
316
+ }
317
+ }
318
+ }
319
+ \`\`\`
320
+ `,
321
+ canvas: `
322
+ ## Canvas API — FTC Dashboard Field Overlay
323
+
324
+ The Canvas is accessed via \`packet.fieldOverlay()\` on a \`TelemetryPacket\`. All Canvas methods are **fluent/chainable** (they return \`this\`).
325
+
326
+ ### Coordinate System
327
+ - Units: **inches**
328
+ - Origin: **center of the field**
329
+ - **+X**: toward the right when facing the red alliance wall
330
+ - **+Y**: away from the red alliance wall (toward the blue alliance wall)
331
+ - The default field image is 144 x 144 inches (12 ft x 12 ft)
332
+
333
+ ### Shape Methods (Exact Signatures)
334
+
335
+ | Method | Description |
336
+ |--------|-------------|
337
+ | \`fillCircle(double x, double y, double r)\` | Filled circle at (x, y) with radius r |
338
+ | \`strokeCircle(double x, double y, double r)\` | Outlined circle at (x, y) with radius r |
339
+ | \`fillRect(double x, double y, double w, double h)\` | Filled rectangle — (x, y) is the CENTER, w and h are full width/height |
340
+ | \`strokeRect(double x, double y, double w, double h)\` | Outlined rectangle — (x, y) is the CENTER |
341
+ | \`fillPolygon(double[] xPoints, double[] yPoints)\` | Filled polygon from coordinate arrays |
342
+ | \`strokePolygon(double[] xPoints, double[] yPoints)\` | Outlined polygon from coordinate arrays |
343
+ | \`strokePolyline(double[] xPoints, double[] yPoints)\` | Open polyline (not closed) from coordinate arrays |
344
+ | \`strokeLine(double x1, double y1, double x2, double y2)\` | Line segment from (x1,y1) to (x2,y2) |
345
+
346
+ ### Text Methods
347
+
348
+ | Method | Description |
349
+ |--------|-------------|
350
+ | \`fillText(String text, double x, double y, String font, double theta)\` | Filled text at (x,y), rotated by theta radians |
351
+ | \`strokeText(String text, double x, double y, String font, double theta)\` | Outlined text at (x,y), rotated by theta radians |
352
+ | \`fillText(String text, double x, double y, String font, double theta, boolean usePageFrame)\` | With explicit page frame flag |
353
+ | \`strokeText(String text, double x, double y, String font, double theta, boolean usePageFrame)\` | With explicit page frame flag |
354
+
355
+ - \`font\`: CSS font string, e.g. \`"16px monospace"\`, \`"bold 12px sans-serif"\`
356
+ - \`theta\`: rotation in radians
357
+ - \`usePageFrame\`: when \`true\`, coordinates are in page/pixel frame (ignores field transforms); when \`false\` (default), coordinates are in the field/transform frame
358
+
359
+ ### Image Drawing
360
+ \`\`\`java
361
+ canvas.drawImage(String path, double x, double y, double w, double h)
362
+ canvas.drawImage(String path, double x, double y, double w, double h, boolean usePageFrame)
363
+ \`\`\`
364
+ - Custom images go in: \`TeamCode/src/main/assets/images/\`
365
+ - \`path\` is relative to the assets directory, e.g. \`"/images/myRobot.png"\`
366
+ - (x, y) is the center; w and h are full dimensions in inches
367
+
368
+ ### Style Methods
369
+
370
+ | Method | Description |
371
+ |--------|-------------|
372
+ | \`setFill(String cssColor)\` | Set fill color — any CSS color: \`"red"\`, \`"#FF0000"\`, \`"rgba(255,0,0,0.5)"\` |
373
+ | \`setStroke(String cssColor)\` | Set stroke/outline color |
374
+ | \`setStrokeWidth(int px)\` | Set stroke width in pixels |
375
+ | \`setAlpha(double alpha)\` | Set global transparency: 0.0 (invisible) to 1.0 (opaque) |
376
+
377
+ ### Transform Methods
378
+ Each transform method **OVERRIDES** the previous transform of that type (they do NOT compose/accumulate).
379
+
380
+ | Method | Description |
381
+ |--------|-------------|
382
+ | \`setTranslation(double x, double y)\` | Translate subsequent drawings by (x, y) |
383
+ | \`setRotation(double theta)\` | Rotate subsequent drawings by theta radians |
384
+ | \`setScale(double sx, double sy)\` | Scale subsequent drawings |
385
+
386
+ To combine transforms, apply them before drawing. They affect all subsequent draw calls until changed.
387
+
388
+ ### Common Pattern: Drawing a Robot on the Field
389
+ \`\`\`java
390
+ TelemetryPacket packet = new TelemetryPacket();
391
+ Canvas canvas = packet.fieldOverlay();
392
+
393
+ double robotX = 24.0; // inches from center
394
+ double robotY = -36.0; // inches from center
395
+ double heading = Math.toRadians(45); // radians
396
+ double robotRadius = 9.0; // inches (typical 18" robot)
397
+
398
+ // Draw robot body
399
+ canvas.setStroke("blue")
400
+ .setStrokeWidth(2)
401
+ .strokeCircle(robotX, robotY, robotRadius);
402
+
403
+ // Draw heading indicator line
404
+ canvas.setStroke("green")
405
+ .strokeLine(
406
+ robotX, robotY,
407
+ robotX + robotRadius * Math.cos(heading),
408
+ robotY + robotRadius * Math.sin(heading)
409
+ );
410
+
411
+ dashboard.sendTelemetryPacket(packet);
412
+ \`\`\`
413
+
414
+ ### Common Pattern: Drawing a Path/Trajectory
415
+ \`\`\`java
416
+ TelemetryPacket packet = new TelemetryPacket();
417
+ Canvas canvas = packet.fieldOverlay();
418
+
419
+ // Define waypoints
420
+ double[] xPoints = {0, 12, 24, 36, 48};
421
+ double[] yPoints = {0, 12, 12, 0, -12};
422
+
423
+ // Draw the path as a polyline
424
+ canvas.setStroke("red")
425
+ .setStrokeWidth(1)
426
+ .strokePolyline(xPoints, yPoints);
427
+
428
+ // Highlight waypoints
429
+ canvas.setFill("yellow")
430
+ .setStroke("black")
431
+ .setStrokeWidth(1);
432
+ for (int i = 0; i < xPoints.length; i++) {
433
+ canvas.fillCircle(xPoints[i], yPoints[i], 2);
434
+ canvas.strokeCircle(xPoints[i], yPoints[i], 2);
435
+ }
436
+
437
+ dashboard.sendTelemetryPacket(packet);
438
+ \`\`\`
439
+
440
+ ### Complete Example with Transforms
441
+ \`\`\`java
442
+ TelemetryPacket packet = new TelemetryPacket();
443
+ Canvas canvas = packet.fieldOverlay();
444
+
445
+ // Draw a robot-shaped rectangle rotated to the robot's heading
446
+ double robotX = 10, robotY = 20;
447
+ double heading = Math.toRadians(30);
448
+
449
+ canvas.setTranslation(robotX, robotY)
450
+ .setRotation(heading)
451
+ .setStroke("blue")
452
+ .setStrokeWidth(2)
453
+ .strokeRect(0, 0, 18, 18) // 18x18 inch robot centered at transform origin
454
+ .setStroke("red")
455
+ .strokeLine(0, 0, 12, 0); // heading arrow
456
+
457
+ // Reset transforms for other drawings
458
+ canvas.setTranslation(0, 0)
459
+ .setRotation(0);
460
+
461
+ // Draw field elements in absolute coordinates
462
+ canvas.setFill("yellow")
463
+ .fillCircle(0, 0, 3); // mark field center
464
+
465
+ dashboard.sendTelemetryPacket(packet);
466
+ \`\`\`
467
+ `,
468
+ camera: `
469
+ ## Camera Streaming to FTC Dashboard
470
+
471
+ ### Method 1: VisionPortal Processor (Recommended for modern FTC SDK)
472
+
473
+ Create a class that implements both \`VisionProcessor\` (for VisionPortal) and \`CameraStreamSource\` (for dashboard):
474
+
475
+ \`\`\`java
476
+ import android.graphics.Bitmap;
477
+ import android.graphics.Canvas;
478
+
479
+ import com.acmerobotics.dashboard.FtcDashboard;
480
+ import com.acmerobotics.dashboard.config.Config;
481
+
482
+ import org.firstinspires.ftc.robotcore.external.function.Consumer;
483
+ import org.firstinspires.ftc.robotcore.external.function.Continuation;
484
+ import org.firstinspires.ftc.robotcore.external.stream.CameraStreamSource;
485
+ import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
486
+ import org.firstinspires.ftc.vision.VisionProcessor;
487
+ import org.opencv.android.Utils;
488
+ import org.opencv.core.Mat;
489
+
490
+ import java.util.concurrent.atomic.AtomicReference;
491
+
492
+ public class CameraStreamProcessor implements VisionProcessor, CameraStreamSource {
493
+ private final AtomicReference<Bitmap> lastFrame = new AtomicReference<>(
494
+ Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
495
+ );
496
+
497
+ @Override
498
+ public void init(int width, int height, CameraCalibration calibration) {
499
+ // Called when the processor is initialized
500
+ }
501
+
502
+ @Override
503
+ public Object processFrame(Mat frame, long captureTimeNanos) {
504
+ // Convert the OpenCV Mat to an Android Bitmap
505
+ Bitmap bitmap = Bitmap.createBitmap(frame.width(), frame.height(), Bitmap.Config.RGB_565);
506
+ Utils.matToBitmap(frame, bitmap);
507
+ lastFrame.set(bitmap);
508
+ return null; // No user context needed
509
+ }
510
+
511
+ @Override
512
+ public void onDrawFrame(Canvas canvas, int onscreenWidth, int onscreenHeight,
513
+ float scaleBmpPxToCanvasPx, float scaleCanvasDensity,
514
+ Object userContext) {
515
+ // Optional: draw annotations on the DS preview
516
+ }
517
+
518
+ @Override
519
+ public void getFrameBitmap(Continuation<? extends Consumer<Bitmap>> continuation) {
520
+ continuation.dispatch(new Consumer<Bitmap>() {
521
+ @Override
522
+ public void accept(Bitmap bitmapConsumer) {
523
+ bitmapConsumer = lastFrame.get();
524
+ }
525
+ });
526
+ }
527
+ }
528
+ \`\`\`
529
+
530
+ **Using it in your OpMode:**
531
+ \`\`\`java
532
+ import com.acmerobotics.dashboard.FtcDashboard;
533
+ import org.firstinspires.ftc.vision.VisionPortal;
534
+
535
+ @TeleOp(name = "Camera Stream Demo")
536
+ public class CameraStreamDemo extends LinearOpMode {
537
+ @Override
538
+ public void runOpMode() {
539
+ CameraStreamProcessor cameraProcessor = new CameraStreamProcessor();
540
+
541
+ VisionPortal portal = new VisionPortal.Builder()
542
+ .addProcessor(cameraProcessor)
543
+ .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
544
+ .build();
545
+
546
+ FtcDashboard.getInstance().startCameraStream(cameraProcessor, 30);
547
+
548
+ waitForStart();
549
+
550
+ while (opModeIsActive()) {
551
+ telemetry.addLine("Streaming to dashboard...");
552
+ telemetry.update();
553
+ }
554
+
555
+ // Camera stream auto-stops when OpMode ends
556
+ portal.close();
557
+ }
558
+ }
559
+ \`\`\`
560
+
561
+ ### Method 2: EasyOpenCV (OpenCvWebcam)
562
+ \`\`\`java
563
+ import com.acmerobotics.dashboard.FtcDashboard;
564
+ import org.openftc.easyopencv.OpenCvCamera;
565
+ import org.openftc.easyopencv.OpenCvCameraFactory;
566
+ import org.openftc.easyopencv.OpenCvCameraRotation;
567
+ import org.openftc.easyopencv.OpenCvWebcam;
568
+
569
+ @TeleOp(name = "EasyOpenCV Stream")
570
+ public class EasyOpenCVStream extends LinearOpMode {
571
+ @Override
572
+ public void runOpMode() {
573
+ int cameraMonitorViewId = hardwareMap.appContext.getResources()
574
+ .getIdentifier("cameraMonitorViewId", "id", hardwareMap.appContext.getPackageName());
575
+
576
+ OpenCvWebcam webcam = OpenCvCameraFactory.getInstance()
577
+ .createWebcam(hardwareMap.get(WebcamName.class, "Webcam 1"), cameraMonitorViewId);
578
+
579
+ webcam.setPipeline(new MyPipeline());
580
+
581
+ webcam.openCameraDeviceAsync(new OpenCvCamera.AsyncCameraOpenListener() {
582
+ @Override
583
+ public void onOpened() {
584
+ webcam.startStreaming(320, 240, OpenCvCameraRotation.UPRIGHT);
585
+ }
586
+
587
+ @Override
588
+ public void onError(int errorCode) {
589
+ telemetry.addData("Camera Error", errorCode);
590
+ telemetry.update();
591
+ }
592
+ });
593
+
594
+ // OpenCvWebcam implements CameraStreamSource
595
+ FtcDashboard.getInstance().startCameraStream(webcam, 0); // 0 = unlimited FPS
596
+
597
+ waitForStart();
598
+
599
+ while (opModeIsActive()) {
600
+ sleep(20);
601
+ }
602
+
603
+ webcam.stopStreaming();
604
+ }
605
+ }
606
+ \`\`\`
607
+
608
+ ### Method 3: Limelight 3A
609
+ \`\`\`java
610
+ import com.acmerobotics.dashboard.FtcDashboard;
611
+ import com.qualcomm.hardware.limelightvision.Limelight3A;
612
+
613
+ @TeleOp(name = "Limelight Stream")
614
+ public class LimelightStream extends LinearOpMode {
615
+ @Override
616
+ public void runOpMode() {
617
+ Limelight3A limelight = hardwareMap.get(Limelight3A.class, "limelight");
618
+ limelight.start();
619
+
620
+ // Limelight3A implements CameraStreamSource
621
+ FtcDashboard.getInstance().startCameraStream(limelight, 0);
622
+
623
+ waitForStart();
624
+
625
+ while (opModeIsActive()) {
626
+ sleep(20);
627
+ }
628
+
629
+ limelight.stop();
630
+ }
631
+ }
632
+ \`\`\`
633
+
634
+ ### Camera Stream API
635
+ \`\`\`java
636
+ FtcDashboard dashboard = FtcDashboard.getInstance();
637
+
638
+ // Start streaming from any CameraStreamSource
639
+ dashboard.startCameraStream(source, maxFps); // maxFps: 0 = unlimited
640
+
641
+ // Stop the camera stream
642
+ dashboard.stopCameraStream();
643
+
644
+ // Send a single Bitmap image (useful for processed frames)
645
+ dashboard.sendImage(bitmap);
646
+
647
+ // Set JPEG compression quality (0-100, default 50)
648
+ dashboard.setImageQuality(75);
649
+ \`\`\`
650
+
651
+ ### Important Notes
652
+ - The camera stream **auto-stops** when the OpMode ends.
653
+ - \`maxFps\` of \`0\` means unlimited — frames are sent as fast as they are produced.
654
+ - Lower \`setImageQuality()\` values reduce bandwidth but degrade image quality.
655
+ - Only ONE camera stream can be active at a time. Starting a new stream replaces the previous one.
656
+ `,
657
+ setup: `
658
+ ## FTC Dashboard Installation & Setup
659
+
660
+ ### Gradle Configuration
661
+
662
+ **Step 1:** Add the Maven repository to \`build.dependencies.gradle\` (or your project-level \`build.gradle\`):
663
+ \`\`\`groovy
664
+ repositories {
665
+ maven { url = 'https://maven.brott.dev/' }
666
+ }
667
+ \`\`\`
668
+
669
+ **Step 2:** Add the dependency (in your TeamCode \`build.gradle\`):
670
+ \`\`\`groovy
671
+ dependencies {
672
+ implementation 'com.acmerobotics.dashboard:dashboard:0.5.1'
673
+ }
674
+ \`\`\`
675
+
676
+ ### Accessing the Dashboard
677
+
678
+ | Connection Method | URL |
679
+ |-------------------|-----|
680
+ | **Control Hub** (Wi-Fi Direct) | \`http://192.168.43.1:8080/dash\` |
681
+ | **Phone** (Wi-Fi Direct) | \`http://192.168.49.1:8080/dash\` |
682
+
683
+ Open the URL in any modern web browser (Chrome recommended) on a device connected to the Robot Controller's Wi-Fi network.
684
+
685
+ ### Dashboard Features
686
+ - **Browser OpMode Controls**: Start, stop, and init OpModes directly from the dashboard web interface without needing a Driver Station.
687
+ - **Gamepad Support**: Connect a gamepad to the computer running the dashboard browser; the dashboard forwards gamepad input to the robot as gamepad1/gamepad2.
688
+ - **Live Telemetry Graphs**: Numeric telemetry values are automatically graphed over time.
689
+ - **Configuration Panel**: Live-edit \`@Config\` variables without redeploying code.
690
+ - **Camera Stream**: View camera feed in the browser.
691
+ - **Field Overlay**: Draw on a top-down field diagram in real time.
692
+
693
+ ### Disabling for Competition (RS09)
694
+ FTC rule RS09 prohibits wireless communication to non-approved devices during competition. The dashboard must be disabled before competing:
695
+
696
+ \`\`\`java
697
+ // In your OpMode or robot initialization
698
+ FtcDashboard dashboard = FtcDashboard.getInstance();
699
+ // Dashboard is enabled by default; no special action needed for practice
700
+ // For competition, disable the dashboard to comply with RS09
701
+ \`\`\`
702
+
703
+ The simplest approach: do NOT connect to the dashboard network during matches. The dashboard only activates when a browser client connects. Alternatively, you can avoid including the dashboard dependency in your competition build, or call \`FtcDashboard.getInstance().setEnabled(false)\` at init.
704
+
705
+ ### Complete Minimal OpMode with Dashboard
706
+ \`\`\`java
707
+ import com.acmerobotics.dashboard.FtcDashboard;
708
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
709
+ import com.acmerobotics.dashboard.config.Config;
710
+ import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
711
+ import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
712
+
713
+ @Config
714
+ @TeleOp(name = "Dashboard Quickstart")
715
+ public class DashboardQuickstart extends LinearOpMode {
716
+ public static double MOTOR_POWER = 0.5;
717
+ public static int TARGET_POSITION = 1000;
718
+
719
+ @Override
720
+ public void runOpMode() {
721
+ telemetry = new MultipleTelemetry(telemetry, FtcDashboard.getInstance().getTelemetry());
722
+
723
+ waitForStart();
724
+
725
+ while (opModeIsActive()) {
726
+ telemetry.addData("Motor Power", MOTOR_POWER);
727
+ telemetry.addData("Target", TARGET_POSITION);
728
+ telemetry.addData("Time", "%.1f s", getRuntime());
729
+ telemetry.update();
730
+ }
731
+ }
732
+ }
733
+ \`\`\`
734
+ `,
735
+ api: `
736
+ ## FtcDashboard Public API Summary
737
+
738
+ \`\`\`java
739
+ import com.acmerobotics.dashboard.FtcDashboard;
740
+
741
+ FtcDashboard dashboard = FtcDashboard.getInstance();
742
+ \`\`\`
743
+
744
+ ### Singleton Access
745
+
746
+ | Method | Description |
747
+ |--------|-------------|
748
+ | \`static FtcDashboard getInstance()\` | Returns the singleton dashboard instance. Available after SDK initialization, during init/start/loop. |
749
+
750
+ ### Telemetry
751
+
752
+ | Method | Description |
753
+ |--------|-------------|
754
+ | \`void sendTelemetryPacket(TelemetryPacket packet)\` | Send a telemetry packet with key-value data and optional field overlay |
755
+ | \`void clearTelemetry()\` | Clear all telemetry data displayed on the dashboard |
756
+ | \`Telemetry getTelemetry()\` | Get a standard Telemetry adapter that forwards to the dashboard |
757
+ | \`void setTelemetryTransmissionInterval(int ms)\` | Set how often telemetry is sent to the browser (default: 100ms) |
758
+
759
+ ### Camera Streaming
760
+
761
+ | Method | Description |
762
+ |--------|-------------|
763
+ | \`void startCameraStream(CameraStreamSource source, double maxFps)\` | Start streaming from a CameraStreamSource; maxFps 0 = unlimited |
764
+ | \`void stopCameraStream()\` | Stop the active camera stream |
765
+ | \`void sendImage(Bitmap bitmap)\` | Send a single Bitmap image to the dashboard |
766
+ | \`void setImageQuality(int quality)\` | Set JPEG quality 0-100 (default: 50) |
767
+
768
+ ### Configuration Variables
769
+
770
+ | Method | Description |
771
+ |--------|-------------|
772
+ | \`void addConfigVariable(String group, String name, ValueProvider<?> provider)\` | Register a programmatic config variable under the given group |
773
+ | \`void removeConfigVariable(String group, String name)\` | Remove a previously registered config variable |
774
+ | \`void updateConfig()\` | Force a config update — pushes current static field values to connected clients |
775
+
776
+ ### OpMode & State Control
777
+
778
+ | Method | Description |
779
+ |--------|-------------|
780
+ | \`boolean isEnabled()\` | Returns whether the dashboard is currently enabled |
781
+ | \`void suppressOpMode(boolean suppress)\` | When true, suppresses the current OpMode from appearing in the dashboard OpMode list |
782
+
783
+ ### Typical Usage Pattern
784
+ \`\`\`java
785
+ import com.acmerobotics.dashboard.FtcDashboard;
786
+ import com.acmerobotics.dashboard.telemetry.MultipleTelemetry;
787
+ import com.acmerobotics.dashboard.telemetry.TelemetryPacket;
788
+ import com.acmerobotics.dashboard.config.Config;
789
+ import com.acmerobotics.dashboard.config.ValueProvider;
790
+
791
+ @Config
792
+ @TeleOp(name = "Full API Demo")
793
+ public class FullApiDemo extends LinearOpMode {
794
+ public static double GAIN = 1.0;
795
+ public static int THRESHOLD = 100;
796
+
797
+ @Override
798
+ public void runOpMode() {
799
+ FtcDashboard dashboard = FtcDashboard.getInstance();
800
+
801
+ // Option A: MultipleTelemetry for simple key-value data
802
+ telemetry = new MultipleTelemetry(telemetry, dashboard.getTelemetry());
803
+
804
+ // Register a dynamic config variable
805
+ dashboard.addConfigVariable("Sensors", "brightness", new ValueProvider<Integer>() {
806
+ @Override
807
+ public Integer get() {
808
+ return 50; // read from sensor
809
+ }
810
+
811
+ @Override
812
+ public void set(Integer value) {
813
+ // apply brightness
814
+ }
815
+ });
816
+
817
+ waitForStart();
818
+
819
+ while (opModeIsActive()) {
820
+ // Option B: Full packet with field overlay
821
+ TelemetryPacket packet = new TelemetryPacket();
822
+ packet.put("gain", GAIN);
823
+ packet.put("threshold", THRESHOLD);
824
+
825
+ packet.fieldOverlay()
826
+ .setFill("green")
827
+ .fillCircle(0, 0, 9);
828
+
829
+ dashboard.sendTelemetryPacket(packet);
830
+
831
+ // Driver Station telemetry (using original, not MultipleTelemetry to avoid double-send)
832
+ // If you used MultipleTelemetry above AND sendTelemetryPacket, comment out one approach.
833
+ telemetry.addData("Gain", GAIN);
834
+ telemetry.update();
835
+ }
836
+
837
+ // Cleanup
838
+ dashboard.removeConfigVariable("Sensors", "brightness");
839
+ }
840
+ }
841
+ \`\`\`
842
+ `,
843
+ };