@OnHardwareThread
@OnHardwareThread marks a @SubscribedTo method as hardware-touching. The binder re-routes the handler to the orchestrator’s single dedicated hardware thread, where it executes strictly serially with every other piece of hardware work.
@SubscribedTo(topic = "intake/set/power")
@OnHardwareThread
public void onTarget(double power) {
intake.run(m -> m.setPower(power));
}
Attributes
None. It is a pure marker, and it only affects methods that also carry @SubscribedTo.
Why a dedicated thread
FTC hardware is not thread-safe. The Lynx bus, DcMotorEx, servos, the IMU, and I²C sensors all misbehave when driven from two threads — race conditions, garbled serial responses, ConcurrentModificationException crashes deep in the SDK. The orchestrator owns exactly one thread for hardware, and @OnHardwareThread is how a callback opts into it.
The same thread serves:
@OnHardwareThreadsubscribers,@RunPeriodically(hardware = true)loops,HardwareActions.run/call/callAsync/bulkReadsubmissions.
Work on it is strictly serial — no locks are needed anywhere in user code.
What it does not do
- It does not intercept hardware calls made from unannotated code. A raw
motor.setPower(...)inside a plain@SubscribedTomethod still runs on the callback pool. - It does not protect
onSafeLoop().SafeOpMode.loop()asserts its own thread; if your OpMode loop touches hardware directly, that touch happens off-thread. Move it into a node, or useSafeDevice.run(...)from the loop. - It does not apply to
@RunnableActionmethods — actions always run on the action pool.
The deadlock rule
Never block waiting for the hardware thread while on it. Calling
hardware.call(...)ordevice.call(...)inside an@OnHardwareThreadsubscriber, ahardware = trueloop, or a bulk-read reader hangs the robot: the one thread is waiting for a task it cannot start. Code already on the hardware thread should usedevice.raw()and touch the device directly.
Next steps
- HardwareActions — routing hardware work from anywhere else.
- Architecture — the full threading picture.