Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions skill/synapse-pubsub-ftc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ 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()` |
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
| `init_loop()` | `onSafeInitLoop()` | rarely used |
| `start()` | `onSafeStart()` | one-shot start logic |
| `loop()` | `onSafeLoop()` | telemetry from topics only |
| `stop()` | `onSafeStop()` (before close) | flush telemetry; orchestrator closes right after |

Every thread is a daemon named `synapse-<name>-<n>`; 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.

---
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,13 +71,33 @@ public abstract class SafeOpMode extends OpMode {
/** Wraps {@code hardwareMap} so device lookups return {@link SafeDevice}s. */
protected SafeHardwareMap safeMap;

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug low

@TELEOP not @inherited causes publishGamepads default to fail for concrete subclasses

The default publishGamepads check fails for concrete subclasses because @TeleOp isn't @Inherited. Document in the javadoc that the annotation must be on the concrete OpMode class.

Prompt for LLM
File src/main/java/com/aaravlabs/synapse/ftc/SafeOpMode.java, line 74:

@TeleOp not @Inherited causes publishGamepads default to fail for concrete subclasses

The @TeleOp annotation is not @Inherited, so publishGamepads default fails for concrete subclasses of an @TeleOp-annotated base class. getClass().isAnnotationPresent(TeleOp.class) returns false for a concrete class that doesn't carry @TeleOp itself, even if its parent does. This matches FTC SDK Driver Station behavior but is a silent footgun. Document in the javadoc for publishGamepads that @TeleOp must be present on the concrete OpMode class, not just a parent class.

Suggested code:

/**
 * Whether {@link #attachGamepad(Object, String)} should actually attach a
 * {@link GamepadAdaptor} for this OpMode.
 *
 * <p>Defaulted in {@link #init()} based on the OpMode type:
 * <ul>
 *   <li>{@link TeleOp}-annotated OpModes: {@code true} — gamepads are published
 *       by default.</li>
 *   <li>Autonomous / other OpModes: {@code false} — gamepads are <b>not</b>
 *       published by default; set this to {@code true} in {@link #onSafeInit()}
 *       to opt in.</li>
 * </ul>
 *
 * <p><b>Note:</b> {@link TeleOp} is <b>not</b> {@code @Inherited}. The annotation must be
 * present on the <b>concrete OpMode class</b> (the one instantiated), not just a
 * parent class, for the default to be {@code true}. This matches FTC SDK behavior
 * for Driver Station registration.
 *
 * <p>Subclasses may override this field in {@link #onSafeInit()} before
 * calling {@link #attachGamepad} to opt in (auto) or opt out (teleop).
 */

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

* Whether {@link #attachGamepad(Object, String)} should actually attach a
* {@link GamepadAdaptor} for this OpMode.
*
* <p>Defaulted in {@link #init()} based on the OpMode type:
* <ul>
* <li>{@link TeleOp}-annotated OpModes: {@code true} — gamepads are published
* by default.</li>
* <li>Autonomous / other OpModes: {@code false} — gamepads are <b>not</b>
* published by default; set this to {@code true} in {@link #onSafeInit()}
* to opt in.</li>
* </ul>
*
* <p>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() {
orchestrator = FtcOrchestrator.create();
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();
}

Expand Down Expand Up @@ -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.
*
* <p>Use this instead of calling {@link GamepadAdaptor#attach} directly so
* the auto/teleop default is respected. Example:
*
* <pre>{@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
* }
* }
* }</pre>
*/
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);
}
}
130 changes: 130 additions & 0 deletions src/test/java/com/aaravlabs/synapse/ftc/SafeOpModeTest.java
Original file line number Diff line number Diff line change
@@ -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")
Comment thread
kody-ai[bot] marked this conversation as resolved.
Resolved
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<SafeOpMode> 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");
}
}
36 changes: 36 additions & 0 deletions website/src/content/docs/ftc/gamepad-adaptor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading