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..75473dc 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. --- @@ -125,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)); @@ -211,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/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: + *

+ * + *

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..5c6506b --- /dev/null +++ b/src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java @@ -0,0 +1,130 @@ +package com.aaravlabs.synapse.ftc; + +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import com.qualcomm.robotcore.hardware.Gamepad; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +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"); + } + } + + /** Tracks every OpMode initialized by a test so its orchestrator is closed. */ + private final List initialized = new ArrayList<>(); + + @AfterEach + void tearDown() { + 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"); + } + + @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"); + } + + @Test + void teleOpAttachesGamepadAdaptorByDefault() { + SafeOpMode op = new TeleOpDefault(); + op.gamepad1 = new Gamepad(); + op.init(); + initialized.add(op); + assertTrue(op.publishGamepads); + 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); + 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"); + 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"); + 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 04b9b61..f4800ff 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` | `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 | + +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") +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.