Telemetry with Commands¶
The commands library provides built-in support for telemetry and logging. This allows you to monitor the state of your commands and mechanisms in real time, which is invaluable for debugging unexpected cancellations, priority conflicts, stuck commands, and mode-transition cleanup.
Protobuf Serialization¶
The Scheduler and its running commands can be serialized using Google Protocol Buffers (Protobuf). This is the primary way that command state is sent to external tools like AdvantageScope or the WPILib Dashboards.
The Scheduler implements the ProtobufSerializable interface, which means it can be sent over NetworkTables and logged using the ProtobufLogEntry API or Epilogue if assigned to a field in a logged class.
Scheduler state is a snapshot. It answers questions like “which commands are running right now?” and “which mechanisms do they require?” Scheduler events are a timeline. They answer questions like “why did this command stop?” and “what interrupted it?” In practice, teams often want both.
Scheduler Events¶
The Scheduler emits events whenever something significant happens, such as a command being scheduled, mounted, yielding, completing, or being canceled. You can register an event listener to respond to these events via Scheduler.getDefault().addEventListener(Consumer<SchedulerEvent>).
Event listeners run as part of scheduler processing, so they should be short and nonblocking. Logging a small message is fine. Doing expensive formatting, file loading, network calls, or long calculations in an event listener can delay the scheduler just like expensive command code can.
Event Types¶
All events implement the SchedulerEvent interface and include a timestampMicros() (measured in microseconds since robot start).
Scheduled: A command was added to the scheduler’s “pending” set. This happens when a trigger is activated, a default command is queued, or
Scheduler.schedule()is called manually.Mounted: A running command’s coroutine has been mounted and is about to execute until it yields or completes. This occurs every scheduler cycle for each running command.
Yielded: A running command called
coroutine.yield()(or another yielding method likewait()). It will remain in the running set and resume in the next cycle.Completed: A command finished its execution naturally (the
run()method returned).CompletedWithError: A command encountered an unhandled exception. The event includes the
Throwableerror that caused the failure. An event listener can log this event, but the error will still be thrown and cause the program to crash.Interrupted: A command was interrupted by another command. This event includes the
interruptercommand that caused the interruption.Canceled: A command was removed from the scheduler without finishing naturally. This happens due to an interruption, a manual
Scheduler.cancel()call, or because its enclosing scope exited.
Event Ordering and Co-occurrence¶
Events often occur in specific sequences or together within the same scheduler cycle:
Interruption and Cancellation: When a command is interrupted, an
Interruptedevent is emitted immediately before theCanceledevent.Initial Run: When a command starts for the first time, you will see a
Scheduledevent, followed by aMountedevent when the command first gets CPU time.Natural Completion: A command that finishes naturally will emit a
Completedevent. It does not emit aCanceledevent.Errors: If a command throws an exception, it emits a
CompletedWithErrorevent and is removed from the scheduler. It does not emit aCanceledevent.
Do not treat every Canceled event as a bug. Commands are canceled when they are interrupted by newer or higher-priority commands, when their enclosing scope exits, when a whileTrue binding goes false, or when user code cancels them manually. The surrounding events and the command requirements usually tell you which case happened.
import org.wpilib.command3.Scheduler;
import org.wpilib.command3.SchedulerEvent;
Scheduler.getDefault().addEventListener(event -> {
if (event instanceof SchedulerEvent.Scheduled e) {
System.out.println("Command " + e.command().name() + " was scheduled");
} else if (event instanceof SchedulerEvent.Mounted e) {
// Mounted events occur every cycle - we might not want to log them all!
// System.out.println("Command " + e.command().name() + " mounted");
} else if (event instanceof SchedulerEvent.Yielded e) {
// System.out.println("Command " + e.command().name() + " yielded");
} else if (event instanceof SchedulerEvent.Completed e) {
System.out.println("Command " + e.command().name() + " completed naturally");
} else if (event instanceof SchedulerEvent.CompletedWithError e) {
System.err.println("Command " + e.command().name() + " failed with error: " + e.error());
} else if (event instanceof SchedulerEvent.Interrupted e) {
System.out.println("Command " + e.command().name() + " was interrupted by " + e.interrupter().name());
} else if (event instanceof SchedulerEvent.Canceled e) {
System.out.println("Command " + e.command().name() + " was canceled");
}
});
Data Logging¶
While printing to the console is useful for quick debugging, it is not recommended for persistent storage or deep analysis. For these cases, you should use the standard WPILib data logging APIs to save telemetry to a .wpilog file on the robot.
Logging Scheduler Events¶
You can log individual scheduler events to a data log using a StringLogEntry. This is particularly useful for tracking the exact sequence of command lifecycle events during a match. The event stream is often the fastest way to explain a command that “randomly stopped”: look for an Interrupted event, then inspect the interrupter command and the mechanisms both commands required.
import org.wpilib.command3.Scheduler;
import org.wpilib.command3.SchedulerEvent;
import org.wpilib.datalog.StringLogEntry;
import org.wpilib.system.DataLogManager;
// Create a log entry for scheduler events
StringLogEntry eventLog = new StringLogEntry(DataLogManager.getLog(), "SchedulerEvents");
// Register a listener to log every event
Scheduler.getDefault().addEventListener(event -> {
// Log the string representation of the event
eventLog.append(event.toString());
});
Logging Scheduler State¶
Because the Scheduler class implements ProtobufSerializable, you can log the entire state of the scheduler, including running commands and their mechanisms, using a ProtobufLogEntry. This allows tools like AdvantageScope to visualize the state of the command scheduler over time. Epilogue supports logging protobuf-serializable objects, so storing the scheduler object in a field in the Robot class is an easy way to get automatic logging.
Because the scheduler state changes every loop cycle, you should append the current state to the log or call Epilogue.update in robotPeriodic to ensure telemetry is always updated.
import org.wpilib.command3.Scheduler;
import org.wpilib.epilogue.Epilogue;
import org.wpilib.epilogue.Logged;
import org.wpilib.framework.TimedRobot;
@Logged
public class Robot extends TimedRobot {
private final Scheduler scheduler = Scheduler.getDefault();
@Override
public void robotPeriodic() {
// Run the scheduler
scheduler.run();
// Update telemetry
Epilogue.update(this);
}
}
import org.wpilib.command3.Scheduler;
import org.wpilib.datalog.ProtobufLogEntry;
import org.wpilib.system.DataLogManager;
import org.wpilib.framework.TimedRobot;
public class Robot extends TimedRobot {
// Create a log entry for the scheduler state
private final ProtobufLogEntry<Scheduler> schedulerLog =
ProtobufLogEntry.create(DataLogManager.getLog(), "Scheduler", Scheduler.proto);
@Override
public void robotPeriodic() {
// Run the scheduler
Scheduler.getDefault().run();
// Log the current state of the scheduler
schedulerLog.append(Scheduler.getDefault());
}
}