Conditions and Logging
Every mechanism command so far uses runRepeatedly, so it never ends. A routine needs another way to know when that command has finished, or should be considered finished. Then we log that answer, so you can see it come true.
- An arm or flywheel on Motion Magic, from Motion Magic in Code.
- AdvantageScope installed from Prerequisites.
What mechanism are you working on?
The lesson below is written for the one you pick. Switch back any time to read it for the other.
The arrival question
First, decide what tolerance counts as good enough. The arm is measured as an angle, so its tolerance is an angle too. WPILib's Angle type converts between rotations, degrees and radians for you. Degrees are the easiest of the three to picture, so we will make the tolerance one degree.
First, decide what tolerance counts as good enough. The flywheel is measured as a speed, so its tolerance is a speed too. WPILib's AngularVelocity type converts between rotations a second, RPM and radians a second for you. We will make the tolerance half a rotation a second.
This may take some tuning. Too tight and it never comes true, because the sensor jitters. Too loose and it comes true before the armflywheel is anywhere useful. For the same reason, never check that the reading equals the target exactly: it almost never will.
Next, add three getters. One says where the armflywheel is. One says where it is headed, which we read off the current request. The last says whether it has arrived.
public class Arm implements Mechanism { // ... motor, encoder and positionOut, unchanged // How close counts as "at target". private final Angle tolerance = Degrees.of(1.0); // ... constructor and commands, unchanged /** True when the arm has reached its target angle. */ public boolean isAtTarget() { return getPosition().isNear(getTargetPosition(), tolerance); } /** Current measured arm angle. */ public Angle getPosition() { return encoder.getPosition().getValue(); } /** Angle the arm is currently driving toward. */ public Angle getTargetPosition() { return positionOut.getPositionMeasure(); } // ... setPosition and stopMotor, unchanged}public class Flywheel implements Mechanism { // ... motor and velocityOut, unchanged // How close counts as "at target". private final AngularVelocity tolerance = RotationsPerSecond.of(0.5); // ... constructor and commands, unchanged /** True when the flywheel has reached its target speed. */ public boolean isAtTarget() { return getVelocity().isNear(getTargetVelocity(), tolerance); } /** Current measured flywheel speed. */ public AngularVelocity getVelocity() { return motor.getVelocity().getValue(); } /** Speed the flywheel is currently driving toward. */ public AngularVelocity getTargetVelocity() { return velocityOut.getVelocityMeasure(); } // ... setVelocity and stopMotor, unchanged}Start the log
Recording starts once, at the top of the Robot constructor, and runs from the first loop to the last.
import org.wpilib.driverstation.DriverStation;import org.wpilib.system.DataLogManager; public Robot() { // Record every logged value, plus the driver station, to a .wpilog file. DataLogManager.start(); DriverStation.startDataLog(DataLogManager.getLog());}DataLogManager.start() creates the log file and records every NetworkTables value into it. On the robot it records console output too. DataLogManager.getLog() hands back that same file, and DriverStation.startDataLog writes the driver station's values into it. Those are values that never appear in NetworkTables: whether the robot is enabled, the robot mode, which OpMode is running, and the joystick positions.
Log the mechanism
The armflywheel writes its own signals. Add TelemetryLoggable to the implements line, then write a logTo method that logs the three getters and the motor voltage.
import org.wpilib.telemetry.TelemetryLoggable;import org.wpilib.telemetry.TelemetryTable; public class Arm implements Mechanism, TelemetryLoggable { // ... everything above, unchanged /** Write the arm's signals to the log. Robot calls this every loop. */ @Override public void logTo(TelemetryTable table) { table.log("Position", getPosition()); table.log("TargetPosition", getTargetPosition()); table.log("AtTarget", isAtTarget()); table.log("AppliedVoltage", motor.getMotorVoltage().getValue()); } // ... setPosition and stopMotor, unchanged}import org.wpilib.telemetry.TelemetryLoggable;import org.wpilib.telemetry.TelemetryTable; public class Flywheel implements Mechanism, TelemetryLoggable { // ... everything above, unchanged /** Write the flywheel's signals to the log. Robot calls this every loop. */ @Override public void logTo(TelemetryTable table) { table.log("Velocity", getVelocity()); table.log("TargetVelocity", getTargetVelocity()); table.log("AtTarget", isAtTarget()); table.log("AppliedVoltage", motor.getMotorVoltage().getValue()); } // ... setVelocity and stopMotor, unchanged}A unit value logs in its SI unit, with the unit attached: radians for an angleradians per second for a speed, and volts for the voltage. You do not convert it here. AdvantageScope can convert it to rotations or degreesrotations per second or RPM when you graph it.
Nothing calls logTo yet. Robot does, once per mechanism, every loop, right after the scheduler runs.
import org.wpilib.telemetry.Telemetry; @Overridepublic void robotPeriodic() { Scheduler.getDefault().run(); Telemetry.log("Arm", arm); Telemetry.log("Flywheel", flywheel);}Telemetry.log sees a TelemetryLoggable, makes a table with the name you gave it, and hands that table to logTo. Everything the armflywheel logs lands under Telemetry/ArmFlywheel. robotPeriodic runs in every mode, so the log keeps going whether or not a command is running.
Read the file back
- Start WPILib: Hardware Sim Robot Code, enable Teleoperated, and hold the left triggerright trigger until the armflywheel settles. Let go, then do it once more.
- Disable, then stop the program.
- Open the newest
.wpilogfrom the project'slogsfolder in AdvantageScope. Do it before you start the program again: a file still namedWPILIB_TBD_is deleted on the next start. - Expand
NT:/Telemetry/Arm. PutPositionandTargetPositionon one graph, andAtTargetandAppliedVoltageon a second. - Expand
NT:/Telemetry/Flywheel. PutVelocityandTargetVelocityon one graph, andAtTargetandAppliedVoltageon a second.
You should see
TargetPositionstepping to your target, andPositioncatching up to meet it.TargetVelocitystepping to your target, andVelocityclimbing to meet it.AtTargetturning true once the two meet, and false while the armflywheel is on its way.
A signal that holds one value is not always a bug. Telemetry writes an entry only when the value changes, so a armflywheel that is still records one sample and then nothing until it moves.
Check yourself
Why does isAtTarget() compare against a tolerance instead of checking whether the reading equals the target?
Why does isAtTarget() live in the mechanism's own class instead of being written out wherever a routine needs it?
The log shows the arm settling 2 degrees short of its target, with a 1 degree tolerance. What do you change?
The log shows the flywheel settling at 73 rotations per second against a target of 75, with a 0.5 tolerance. What do you change?
What does DriverStation.startDataLog(DataLogManager.getLog()) add that DataLogManager.start() does not?
You wrote logTo in your mechanism, but the .wpilog has no table for it. What is missing?
You ran the program with WPILib: Hardware Sim Robot Code. Where is the .wpilog?