Gray Matter
WorkshopLogging
Rough draft: nobody has reviewed this lesson yet, and it may not be how things are done this season.
LESSON 21

Logging

Telemetry publishes a number under a name. DataLogManager copies every published value and every console line into one file on disk. You start the recorder in Robot.java, log three signals from your mechanism, then open the file and read them back.

13 minutes
You’ll need
  • The project from Coroutines running on the bench.
  • Robot.java and one mechanism class from the previous lessons.
  • 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.

A failure that lasts a tenth of a second is gone before anyone sees it. The file on disk is the only record. Two lines start the recorder. The rest of the lesson gives it something to record and then reads the file back.

Start the log once

Both calls go at the top of the Robot constructor, which is empty on the branch. Logging then runs from the first loop to the last.

Robot.java: logging starts once
import org.wpilib.driverstation.DriverStation;
import org.wpilib.system.DataLogManager;
 
public Robot() {
DataLogManager.start();
DriverStation.startDataLog(DataLogManager.getLog());
 
// Always-on bindings, if you add any, go below.
}

DataLogManager.start() opens the file and captures NetworkTables values and console output. DriverStation.startDataLog adds what NetworkTables never sees: enabled state, robot mode, which OpMode is running, and joystick positions. Skip the second call and you get numbers with no way to tell whether the robot was enabled when they happened.

Leave logging on in every mode and every build. A special logging build, deployed after the match that went wrong, records the next failure instead of the one you are trying to explain.

Those two lines are only for the file. Watching numbers live needs nothing: RobotBase registers a NetworkTables backend at /Telemetry before your Robot runs, so anything you log reaches the dashboard either way.

Publish three signals

Three signals are enough for a first log, and they are read in pairs. Position against target says whether the arm arrived. Voltage next to either one says what the trip cost, and whether the motor was loaded the whole way.

Three signals are enough for a first log, and they are read in pairs. Velocity against target says whether the wheel is up to speed. Voltage next to either one says what the spin-up cost, and what it takes to hold that speed once a game piece goes through.

Arm.java: three numbers worth keeping
import static org.wpilib.units.Units.Rotations;
 
import org.wpilib.telemetry.Telemetry;
import org.wpilib.telemetry.TelemetryTable;
 
private void record() {
TelemetryTable table = Telemetry.getTable(getName());
 
table.log("PositionRot", getPosition().in(Rotations));
table.log("TargetRot", getTargetPosition().in(Rotations));
table.log("AppliedVolts", motor.getMotorVoltage().getValueAsDouble());
}
Flywheel.java: three numbers worth keeping
import static org.wpilib.units.Units.RotationsPerSecond;
 
import org.wpilib.telemetry.Telemetry;
import org.wpilib.telemetry.TelemetryTable;
 
private void record() {
TelemetryTable table = Telemetry.getTable(getName());
 
table.log("VelocityRPS", getVelocity().in(RotationsPerSecond));
table.log("TargetRPS", getTargetVelocity().in(RotationsPerSecond));
table.log("AppliedVolts", motor.getMotorVoltage().getValueAsDouble());
}

Telemetry.getTable(...) hands back the table for a name, making it on the first call and returning the same one after that. So there is nothing to build in the constructor and nothing to keep in a field. Calling it every loop is the intended use.

getName() is the armflywheel's own name, which Mechanism takes from the class unless you override it. That is what puts all three signals under ArmFlywheel/ without you spelling the prefix into three strings.

.in(Rotations) is where the unit gets decided. Both getters return a WPILib unit type rather than a bare number, and logging one means naming the unit you want it in. Say it here and say it again in the signal name, so the file and the code agree.

Nothing calls record yet. Register it once, as the last line of the ArmFlywheel constructor. The scheduler then runs it every loop while the robot has power. Add import org.wpilib.command3.Scheduler; with it.

Arm.java: last line of the constructor
public Arm() {
// ... the pasted config, unchanged
motor.getConfigurator().apply(talonFXCfg);
Scheduler.getDefault().addPeriodic(() -> record());
}
Flywheel.java: last line of the constructor
public Flywheel() {
// ... the pasted config, unchanged
motor.getConfigurator().apply(talonFXCfg);
Scheduler.getDefault().addPeriodic(() -> record());
}

Do not call record from inside a command instead. A command logs only while it runs, so the trace stops the moment a button comes up.

Signal names

Whoever opens the log at an event may not have written the code. The name in the tree is all they get.

  • Put the unit in the name. Arm/Position makes the reader guess. Arm/PositionRot can share a project with degrees and radians without a collision.
  • Let the table do the grouping. Everything logged to the Arm table arrives together in the viewer, next to Flywheel and Drivetrain. Do not write the prefix into the signal name as well.
  • One writer per fact. Two classes logging PositionRot to the same table give you a trace that flickers between them, and no way to tell which is which.
  • Add a signal when you can name the question it answers. A hundred signals nobody plots is slower to search than twelve that get used.

Rename a signal later and the code still compiles. Every saved layout and every script that read the old name stops working. Spend the extra minute now.

Read the file back

Do this once now, on a run whose answer you already know. Then the first log you open is not one you need in a hurry.

  1. Start the program with WPILib: Hardware Sim Robot Code and enable the OpMode that moves the arm. Send it to a target, let it settle, then send it back.
  2. Start the program with WPILib: Hardware Sim Robot Code and enable the OpMode that spins the flywheel. Take it to full, hold it there long enough to settle, then let it coast down.
  3. Disable, then stop the program, so the end of the file gets written out.
  4. Find the newest .wpilog. The program ran on your laptop, so the file is in the project's logs folder.
  5. Open it in AdvantageScope and expand NT:/Telemetry/Arm. Put PositionRot and TargetRot on one graph, and AppliedVolts on a second.
  6. Open it in AdvantageScope and expand NT:/Telemetry/Flywheel. Put VelocityRPS and TargetRPS on one graph, and AppliedVolts on a second.
  7. Line the enabled interval up against the motion. Position should move only while enabled, and voltage should drop off once the arm arrives.
  8. Line the enabled interval up against the motion. Velocity should climb only while enabled, and voltage should settle to a smaller steady number once the wheel is at speed.

A trace that holds one value is not always a bug. Telemetry writes an entry only when the value changes, so an arm that is genuinely still records one sample and then nothing until it moves. The rest of the file tells you which you have. Every signal stopping at the same instant means the logging stopped. One flat signal among live ones means the thing it measures was flat.

Watch out
Entries reach disk in batches, not one at a time. Kill the program while it is still enabled and the last second or two never gets written, which is usually the part you wanted. Disable, stop the program, and only then cut power.

Three things go wrong the first time, and they look like this.

Empty tree
Nothing published
The file exists and holds no Telemetry/Arm table. Either the two constructor lines never ran, or the addPeriodic line is missing.
Flat line
Stale signal
The trace freezes partway through and holds one value. record is called from a command that finished, not from addPeriodic.
Wrong scale
Bad units
The shape looks right and the numbers are off by the gear ratio. Fix SensorToMechanismRatio on the motor, then log the run again.
Empty tree
Nothing published
The file exists and holds no Telemetry/Flywheel table. Either the two constructor lines never ran, or the addPeriodic line is missing.
Flat line
Stale signal
The trace freezes partway through and holds one value. record is called from a command that finished, not from addPeriodic.
Wrong scale
Bad units
The shape looks right and the numbers are off by the gear ratio. Fix SensorToMechanismRatio on the motor, then log the run again.

Check your work

You are finished when a file on your own laptop can tell you what the armflywheel did, with nobody in the room narrating it.

Check

You should see

  • A Telemetry/ArmFlywheel table in the tree, with all three entries under it.
  • TargetRot stepping to your target, and PositionRot catching up to meet it.
  • TargetRPS stepping to your target, and VelocityRPS climbing to meet it.
  • AppliedVolts large while the arm moves, small while it holds.
  • AppliedVolts large through the spin-up, smaller once the wheel is at speed.
  • The enabled interval covering every part that moves.
WPILib: On-robot telemetry recording

Check yourself

What does DriverStation.startDataLog(DataLogManager.getLog()) add that DataLogManager.start() does not?

The arm knows its position. How does that number reach the .wpilog?

The flywheel knows its speed. How does that number reach the .wpilog?

You ran the program with WPILib: Hardware Sim Robot Code. Where is the .wpilog?

Arm/PositionRot climbs, then freezes partway through the run and holds one value. What happened?

Flywheel/VelocityRPS climbs, then freezes partway through the run and holds one value. What happened?

Pick an answer for each.