From 07fd1c63b51724f980935726ca3897a36185b07d Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 1 Nov 2026 17:07:32 -0600 Subject: [PATCH 1/2] feat(safeopmode): default gamepad publishing by opmode type SafeOpMode.init() now sets publishGamepads based on the class annotation: - @TeleOp -> true (gamepads published by default) - @Autonomous / other -> false (gamepads not published by default; opt in) Add SafeOpMode.attachGamepad(Object, String) which respects the flag, returning the registered node name or null when disabled. Direct GamepadAdaptor.attach() calls are unaffected and remain fully backward compatible. Add SafeOpModeTest covering teleop-on, auto-off, teleop opt-out, and auto opt-in. Document the default in gamepad-adaptor.mdx, SKILL.md, and CHANGELOG.md. --- CHANGELOG.md | 15 +++ skill/synapse-pubsub-ftc/SKILL.md | 4 +- .../com/aaravlabs/synapse/ftc/SafeOpMode.java | 54 ++++++++ .../aaravlabs/synapse/ftc/SafeOpModeTest.java | 119 ++++++++++++++++++ .../src/content/docs/ftc/gamepad-adaptor.mdx | 36 ++++++ 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 377ca8e..162a7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`SafeOpMode.attachGamepad(Object, String)`** — attaches a `GamepadAdaptor` + only when `publishGamepads` is true, so the auto/teleop default is respected. + Returns the registered node name or `null` when gamepad publishing is + disabled for this OpMode. + +### Changed + +- **`SafeOpMode` defaults gamepad publishing by OpMode type.** `init()` now sets + `publishGamepads` to `true` for `@TeleOp` OpModes and `false` for + Autonomous/other OpModes. Gamepads are opt-in for auto OpModes and can be + optionally disabled in teleop by setting `publishGamepads = false` in + `onSafeInit()`. Direct `GamepadAdaptor.attach(...)` calls are unaffected. + ## [0.4.1] - 2026-10-12 ### Fixed diff --git a/skill/synapse-pubsub-ftc/SKILL.md b/skill/synapse-pubsub-ftc/SKILL.md index ca22e7c..60121cf 100644 --- a/skill/synapse-pubsub-ftc/SKILL.md +++ b/skill/synapse-pubsub-ftc/SKILL.md @@ -46,7 +46,7 @@ Plus the OpMode thread itself, which runs the lifecycle. The full map: | OpMode method | Your hook | What you do there | | --- | --- | --- | -| `init()` | `onSafeInit()` (required) | create `SafeDevice`s, register nodes, attach `GamepadAdaptor`s | +| `init()` | `onSafeInit()` (required) | create `SafeDevice`s, register nodes, attach `GamepadAdaptor`s via `attachGamepad()` | | `init_loop()` | `onSafeInitLoop()` | rarely used | | `start()` | `onSafeStart()` | one-shot start logic | | `loop()` | `onSafeLoop()` | telemetry from topics only | @@ -54,6 +54,8 @@ Plus the OpMode thread itself, which runs the lifecycle. The full map: Every thread is a daemon named `synapse--`; the hardware thread is `synapse-ftc-hw-1` — filter Logcat by thread name when something hangs. +**Gamepad publishing defaults.** `SafeOpMode.init()` sets `publishGamepads` to `true` for `@TeleOp` OpModes and `false` for Autonomous/other. Use `attachGamepad(gamepad, "g1")` instead of calling `GamepadAdaptor.attach` directly so the default is respected. Set `publishGamepads = true` in `onSafeInit()` to opt in (auto), or `publishGamepads = false` to opt out (teleop). + **Publishing flow:** `publish()` updates the topic's latest-value cache *synchronously on the caller's thread*, then enqueues subscriber callbacks onto the callback pool (or the hardware thread for `@OnHardwareThread` subscribers). Publishing is always non-blocking — even from the hardware thread. --- diff --git a/src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java b/src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java index 04c9a7d..e226f56 100644 --- a/src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java +++ b/src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java @@ -2,6 +2,7 @@ import com.aaravlabs.synapse.Orchestrator; import com.qualcomm.robotcore.eventloop.opmode.OpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; /** * Base class for {@link OpMode} subclasses that use the Synapse pub/sub bus @@ -70,6 +71,24 @@ public abstract class SafeOpMode extends OpMode { /** Wraps {@code hardwareMap} so device lookups return {@link SafeDevice}s. */ protected SafeHardwareMap safeMap; + /** + * Whether {@link #attachGamepad(Object, String)} should actually attach a + * {@link GamepadAdaptor} for this OpMode. + * + *

Defaulted in {@link #init()} based on the OpMode type: + *

    + *
  • {@link TeleOp}-annotated OpModes: {@code true} — gamepads are published + * by default.
  • + *
  • Autonomous / other OpModes: {@code false} — gamepads are not + * published by default; set this to {@code true} in {@link #onSafeInit()} + * to opt in.
  • + *
+ * + *

Subclasses may override this field in {@link #onSafeInit()} before + * calling {@link #attachGamepad} to opt in (auto) or opt out (teleop). + */ + protected boolean publishGamepads; + @Override @SuppressWarnings("deprecation") public final void init() { @@ -77,6 +96,8 @@ public final void init() { orch = orchestrator; hardware = orchestrator.hardware(); safeMap = new SafeHardwareMap(hardwareMap, hardware); + // Gamepad publishing is on by default for TeleOp, off for Autonomous/other. + publishGamepads = getClass().isAnnotationPresent(TeleOp.class); onSafeInit(); } @@ -119,4 +140,37 @@ protected void onSafeStart() {} /** Called when the OpMode stops, before orchestrator is closed. Defaults to no-op. */ protected void onSafeStop() {} + + // ---- gamepad attachment --------------------------------------------- + + /** + * Attach a {@link GamepadAdaptor} for {@code gamepad} only if + * {@link #publishGamepads} is true. Returns the registered node name + * (e.g. {@code "GamepadAdaptor:g1"}) or {@code null} when gamepad + * publishing is disabled for this OpMode. + * + *

Use this instead of calling {@link GamepadAdaptor#attach} directly so + * the auto/teleop default is respected. Example: + * + *

{@code
+     * @Autonomous(name = "My Auto")
+     * public class MyAuto extends SafeOpMode {
+     *     @Override protected void onSafeInit() {
+     *         // publishGamepads defaults to false for Autonomous.
+     *         attachGamepad(gamepad1, "g1");   // no-op: gamepads not published
+     *         publishGamepads = true;          // opt in
+     *         attachGamepad(gamepad1, "g1");   // now attaches
+     *     }
+     * }
+     * }
+ */ + protected String attachGamepad(Object gamepad, String parentTopic) { + if (!publishGamepads) { + if (orchestrator != null) { + orchestrator.log("GamepadAdaptor: skipping '" + parentTopic + "' — gamepad publishing disabled"); + } + return null; + } + return GamepadAdaptor.attach(orchestrator, gamepad, parentTopic); + } } diff --git a/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java b/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java new file mode 100644 index 0000000..33700f3 --- /dev/null +++ b/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java @@ -0,0 +1,119 @@ +package com.aaravlabs.synapse.ftc; + +import com.aaravlabs.synapse.Orchestrator; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies that {@link SafeOpMode} defaults gamepad publishing off for + * Autonomous/other opmodes (opt-in) and on for {@link TeleOp} opmodes + * (optionally disabled), and that {@link SafeOpMode#attachGamepad(Object, String)} + * respects the {@link SafeOpMode#publishGamepads} flag. + */ +class SafeOpModeTest { + + @TeleOp(name = "TeleOp Default") + private static class TeleOpDefault extends SafeOpMode { + @Override + protected void onSafeInit() { + attachGamepad(gamepad1, "g1"); + } + } + + private static class AutoDefault extends SafeOpMode { + @Override + protected void onSafeInit() { + attachGamepad(gamepad1, "g1"); + } + } + + @TeleOp(name = "TeleOp Opt Out") + private static class TeleOpOptOut extends SafeOpMode { + @Override + protected void onSafeInit() { + publishGamepads = false; // opt out + attachGamepad(gamepad1, "g1"); + } + } + + private static class AutoOptIn extends SafeOpMode { + @Override + protected void onSafeInit() { + publishGamepads = true; // opt in + attachGamepad(gamepad1, "g1"); + } + } + + private Orchestrator orchestrator; + + @BeforeEach + void setUp() { + orchestrator = FtcOrchestrator.create(); + } + + @AfterEach + void tearDown() { + if (orchestrator != null) { + orchestrator.close(); + } + } + + @Test + void teleOpDefaultsPublishGamepadsTrue() { + SafeOpMode op = new TeleOpDefault(); + op.init(); + assertTrue(op.publishGamepads, + "@TeleOp opmode should default publishGamepads to true"); + assertNotNull(op.orchestrator, "orchestrator should be initialized"); + } + + @Test + void autoDefaultsPublishGamepadsFalse() { + SafeOpMode op = new AutoDefault(); + op.init(); + assertFalse(op.publishGamepads, + "Autonomous opmode should default publishGamepads to false"); + } + + @Test + void teleOpAttachesGamepadAdaptorByDefault() { + SafeOpMode op = new TeleOpDefault(); + op.init(); + assertTrue(op.publishGamepads); + assertNotNull(op.orchestrator.findTopic("g1/a", Boolean.class), + "gamepad topics should be created for @TeleOp by default"); + } + + @Test + void autoDoesNotAttachGamepadAdaptorByDefault() { + SafeOpMode op = new AutoDefault(); + op.init(); + assertFalse(op.publishGamepads); + assertNull(op.orchestrator.findTopic("g1/a", Boolean.class).orElse(null), + "no gamepad topics should be created for Auto by default"); + } + + @Test + void teleOpOptOutDisablesGamepadPublishing() { + SafeOpMode op = new TeleOpOptOut(); + op.init(); + assertFalse(op.publishGamepads, + "TeleOp with publishGamepads=false should not publish gamepads"); + assertNull(op.orchestrator.findTopic("g1/a", Boolean.class).orElse(null), + "no gamepad topics should be created when opted out"); + } + + @Test + void autoOptInEnablesGamepadPublishing() { + SafeOpMode op = new AutoOptIn(); + op.init(); + assertTrue(op.publishGamepads, + "Auto opmode with publishGamepads=true should publish gamepads"); + assertNotNull(op.orchestrator.findTopic("g1/a", Boolean.class), + "gamepad topics should be created when opted in"); + } +} diff --git a/website/src/content/docs/ftc/gamepad-adaptor.mdx b/website/src/content/docs/ftc/gamepad-adaptor.mdx index 04b9b61..8ade1ef 100644 --- a/website/src/content/docs/ftc/gamepad-adaptor.mdx +++ b/website/src/content/docs/ftc/gamepad-adaptor.mdx @@ -68,6 +68,42 @@ GamepadAdaptor.attach(orchestrator, gamepad2, "g2"); Each adaptor is independent; the parent prefix keeps their topics apart. See the [two-controller recipe](/docs/recipes/two-controller-teleop) for the robot-validated driver/operator split. +## SafeOpMode defaults + +When you extend [`SafeOpMode`](/docs/ftc/safe-opmode), gamepad publishing defaults depend on the OpMode type: + +| OpMode annotation | `publishGamepads` default | Behavior | +|---|---|---| +| `@TeleOp` | `true` | Gamepad adaptors attach automatically via `attachGamepad` | +| `@Autonomous` / none | `false` | Gamepads are **not** published — opt in by setting `publishGamepads = true` | + +Use [`SafeOpMode#attachGamepad(Object, String)`](/docs/ftc/safe-opmode) instead of calling `GamepadAdaptor.attach` directly so the default is respected: + +```java +@TeleOp(name = "Main TeleOp") +public class MainTeleOp extends SafeOpMode { + @Override protected void onSafeInit() { + // publishGamepads defaults to true for @TeleOp. + attachGamepad(gamepad1, "g1"); + attachGamepad(gamepad2, "g2"); + } +} +``` + +```java +@Autonomous(name = "Red Alliance Auto") +public class RedAuto extends SafeOpMode { + @Override protected void onSafeInit() { + // publishGamepads defaults to false for Autonomous. + attachGamepad(gamepad1, "g1"); // no-op: not published + publishGamepads = true; // opt in + attachGamepad(gamepad1, "g1"); // now attached + } +} +``` + +To opt out of gamepad publishing in a `@TeleOp` OpMode, set `publishGamepads = false` in `onSafeInit()` before calling `attachGamepad`. + ## Next steps - [Mecanum drive](/docs/recipes/mecanum-drive) — gamepad topics driving a real drivetrain. From 6f065c84b9db28f9fb39c6664fdf985b174c6bd8 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 1 Nov 2026 17:17:39 -0600 Subject: [PATCH 2/2] fix: address code review comments on gamepad opt-in PR - SafeOpModeTest: remove unused orchestrator fixture; track initialized OpModes and close their orchestrators in tearDown(); use assertTrue(isPresent()) instead of assertNotNull on Optional; set gamepad1 before init() so topic attachment is actually verified - SKILL.md: skeleton OpMode uses attachGamepad(); list publishGamepads and attachGamepad in Quick API surface - gamepad-adaptor.mdx: clarify TeleOp default does not auto-attach; defaults only apply via attachGamepad(); direct GamepadAdaptor.attach calls unaffected --- skill/synapse-pubsub-ftc/SKILL.md | 3 +- .../aaravlabs/synapse/ftc/SafeOpModeTest.java | 39 ++++++++++++------- .../src/content/docs/ftc/gamepad-adaptor.mdx | 6 +-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/skill/synapse-pubsub-ftc/SKILL.md b/skill/synapse-pubsub-ftc/SKILL.md index 60121cf..75473dc 100644 --- a/skill/synapse-pubsub-ftc/SKILL.md +++ b/skill/synapse-pubsub-ftc/SKILL.md @@ -127,7 +127,7 @@ public class MainTeleOp extends SafeOpMode { backLeft = safeMap.device(DcMotorEx.class, "backLeft"); backRight = safeMap.device(DcMotorEx.class, "backRight"); - GamepadAdaptor.attach(orchestrator, gamepad1, "g1"); + attachGamepad(gamepad1, "g1"); orchestrator.registerNode("drive", new Drive(orchestrator, frontLeft, frontRight, backLeft, backRight)); @@ -213,6 +213,7 @@ com.aaravlabs.synapse.annotation com.aaravlabs.synapse.ftc SafeOpMode orchestrator / hardware / safeMap + 5 hooks + publishGamepads / attachGamepad(gamepad, "g1") FtcOrchestrator create() / create(sink) HardwareActions run / call / call(+timeout) / callAsync bulkRead / stopBulkRead / isHardwareThread diff --git a/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java b/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java index 33700f3..5c6506b 100644 --- a/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java +++ b/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java @@ -1,11 +1,13 @@ package com.aaravlabs.synapse.ftc; -import com.aaravlabs.synapse.Orchestrator; import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.Gamepad; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; /** @@ -48,24 +50,23 @@ protected void onSafeInit() { } } - private Orchestrator orchestrator; - - @BeforeEach - void setUp() { - orchestrator = FtcOrchestrator.create(); - } + /** Tracks every OpMode initialized by a test so its orchestrator is closed. */ + private final List initialized = new ArrayList<>(); @AfterEach void tearDown() { - if (orchestrator != null) { - orchestrator.close(); + for (SafeOpMode op : initialized) { + op.stop(); } + initialized.clear(); } @Test void teleOpDefaultsPublishGamepadsTrue() { SafeOpMode op = new TeleOpDefault(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertTrue(op.publishGamepads, "@TeleOp opmode should default publishGamepads to true"); assertNotNull(op.orchestrator, "orchestrator should be initialized"); @@ -74,7 +75,9 @@ void teleOpDefaultsPublishGamepadsTrue() { @Test void autoDefaultsPublishGamepadsFalse() { SafeOpMode op = new AutoDefault(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertFalse(op.publishGamepads, "Autonomous opmode should default publishGamepads to false"); } @@ -82,38 +85,46 @@ void autoDefaultsPublishGamepadsFalse() { @Test void teleOpAttachesGamepadAdaptorByDefault() { SafeOpMode op = new TeleOpDefault(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertTrue(op.publishGamepads); - assertNotNull(op.orchestrator.findTopic("g1/a", Boolean.class), + assertTrue(op.orchestrator.findTopic("g1/a", Boolean.class).isPresent(), "gamepad topics should be created for @TeleOp by default"); } @Test void autoDoesNotAttachGamepadAdaptorByDefault() { SafeOpMode op = new AutoDefault(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertFalse(op.publishGamepads); - assertNull(op.orchestrator.findTopic("g1/a", Boolean.class).orElse(null), + assertFalse(op.orchestrator.findTopic("g1/a", Boolean.class).isPresent(), "no gamepad topics should be created for Auto by default"); } @Test void teleOpOptOutDisablesGamepadPublishing() { SafeOpMode op = new TeleOpOptOut(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertFalse(op.publishGamepads, "TeleOp with publishGamepads=false should not publish gamepads"); - assertNull(op.orchestrator.findTopic("g1/a", Boolean.class).orElse(null), + assertFalse(op.orchestrator.findTopic("g1/a", Boolean.class).isPresent(), "no gamepad topics should be created when opted out"); } @Test void autoOptInEnablesGamepadPublishing() { SafeOpMode op = new AutoOptIn(); + op.gamepad1 = new Gamepad(); op.init(); + initialized.add(op); assertTrue(op.publishGamepads, "Auto opmode with publishGamepads=true should publish gamepads"); - assertNotNull(op.orchestrator.findTopic("g1/a", Boolean.class), + assertTrue(op.orchestrator.findTopic("g1/a", Boolean.class).isPresent(), "gamepad topics should be created when opted in"); } } diff --git a/website/src/content/docs/ftc/gamepad-adaptor.mdx b/website/src/content/docs/ftc/gamepad-adaptor.mdx index 8ade1ef..f4800ff 100644 --- a/website/src/content/docs/ftc/gamepad-adaptor.mdx +++ b/website/src/content/docs/ftc/gamepad-adaptor.mdx @@ -74,10 +74,10 @@ When you extend [`SafeOpMode`](/docs/ftc/safe-opmode), gamepad publishing defaul | OpMode annotation | `publishGamepads` default | Behavior | |---|---|---| -| `@TeleOp` | `true` | Gamepad adaptors attach automatically via `attachGamepad` | -| `@Autonomous` / none | `false` | Gamepads are **not** published — opt in by setting `publishGamepads = true` | +| `@TeleOp` | `true` | `attachGamepad()` will attach a `GamepadAdaptor` when called; the default does **not** attach anything by itself | +| `@Autonomous` / none | `false` | `attachGamepad()` is a no-op unless `publishGamepads = true` is set first | -Use [`SafeOpMode#attachGamepad(Object, String)`](/docs/ftc/safe-opmode) instead of calling `GamepadAdaptor.attach` directly so the default is respected: +These defaults only take effect when you use [`SafeOpMode#attachGamepad(Object, String)`](/docs/ftc/safe-opmode). Direct `GamepadAdaptor.attach(...)` calls are unaffected and always register an adaptor regardless of OpMode type. ```java @TeleOp(name = "Main TeleOp")