-
Notifications
You must be signed in to change notification settings - Fork 0
Log Fields
The Log Fields are an abstraction over AdvantageKit that simplifies working with the log and IO.
This documentation assumes a basic understanding of AdvantageKit and the concept of IO inputs and replay, and requires the code to have a proper AdvantageKit setup. If you are not familiar with it, please refer to the AdvantageKit documentation.
To enable Log Fields, add the following at the top of robotPeriodic() in Robot.java, before any other code (including CommandScheduler.run()):
LogFieldsTable.updateAllTables();This ensures all registered fields are updated every loop cycle.
First, create a LogFieldsTable, then just call one of the add methods on it with a supplier for the value (e.g., a supplier that gets a value from a sensor). The add methods return another supplier of the same type. This supplier is the one you should use in the rest of your code, as it will return the correct value after saving it to the log in normal mode, and will get the value from the log in replay mode:
LogFieldsTable fieldsTable = new LogFieldsTable("ExampleTable");
DoubleSupplier randomField = fieldsTable.addDouble("randomValue", () -> Math.random() * 10);
// To get the field value, use randomField.getAsDouble()That's it! You can now safely use randomField with the confidence that it will always work correctly with log and replay.
Tip: You can also get the sub-table of a
LogFieldsTableby callinggetSubTable(name). This is useful when you want to create a nested table structure in the log for better organization.
While it is possible to keep recording values using Logger.recordOutput() or using the @AutoLogOutput annotation as usual, with the LogFieldsTable, it is also possible to record outputs with the table prefix:
LogFieldsTable fieldsTable = new LogFieldsTable("ExampleTable");
fieldsTable.recordOutput("number", numberValue);
Logger.recordOutput("ExampleTable/number", numberValue); // this is equivalent to the previous line.Although a matter of preference, this can make the log more organized and easier to work with.
While a separate supplier for each field is sufficient for most cases, in cases where multiple fields depend on the same thing, like in a physics simulation IO (multiple fields may need to use the simulation calculation of each loop cycle), it is possible to set a function that will run periodically before updating the fields:
fieldsTable.setPeriodicBeforeFields(() -> {
// Do something periodically before calling the Log Fields' suppliers.
});By default, the Log Fields will only be updated at the beginning of each loop cycle, each time LogFieldsTable.updateAllTables() is called. Because of that, the fields won't update on the initialization loop cycle by default (the robotInit() cycle where the RobotContainer is usually initialized and all the subsystems are created).
If updating the values for the initialization loop cycle is needed, call fieldsTable.update() manually. Make sure to call it after any initialization that is used for the fields' suppliers (e.g., initializing a sensor object that is used in a field supplier), but before using the fields' values.
When writing an IO layer for some classes, it is often recommended to create a separate IO class. This not only gives a clearer structure and cleaner code, but also allows for multiple implementations of the same IO class. The most common example of this is subsystems, where there might be one implementation for one type of motor controller, another for a different type, and another for a physics simulation.
To easily write separate IO classes, an IOBase class is provided for extra convenience. This is an example of an IO class in the recommended structure:
public abstract class ExampleIO extends IOBase {
public final BooleanSupplier isOn = fields.addBoolean("isOn", this::getIsOn);
public final Supplier<String> text = fields.addString("text", this::getText);
public ExampleIO(LogFieldsTable fieldsTable) {
super(fieldsTable);
}
// inputs
protected abstract boolean getIsOn();
protected abstract String getText();
// outputs
public abstract void turnOn();
}Notice that the inputs getter methods are set to
protected. This is to avoid accidentally using them directly to get the value instead of using the value from the field. The only problem is thatprotectedalso allows access to classes in the same package. For that reason, it's recommended to create aniofolder and put all the IO classes inside it.
Now, to create implementations of that IO, just extend that class and implement the inputs and outputs methods.
To define a periodic function to run before updating the fields, just override the
periodicBeforeFieldsmethod.IOBasesets it to theLogFieldsTableautomatically on creation.
Using this IO is now as simple as creating a LogFieldsTable and passing it to the wanted IO implementation:
LogFieldsTable fieldsTable = new LogFieldsTable("Example");
ExampleIO io = new ExampleIOImpl(fieldsTable);And to use that io:
io.isOn.getAsBoolean();
io.text.get();
io.turnOn();Remember to add
fieldsTable.update()call at the beginning of the constructor if you want to use the fields' values in the initialization loop cycle.
Currently, AdvantageKit is usually used like:
- Defining an abstract IO class.
- Inside it, defining inputs class using
@AutoLogor by manually defining thetoLogandfromLogmethods. - Defining implementations for the IO class by overriding the
updateInputs(inputs)method. - Creating the IO and inputs classes and calling
io.updateInput(inputs)andLogger.processInputs(key, inputs). - Using the values of the inputs object (like
inputs.angle) and methods of the IO object (likeio.setSpeed(1)). - Recording outputs by calling the
Logger.recordOutput(key, value), or by using the@AutoLogOutputannotation.
Using Log Fields has multiple advantages over using AdvantageKit in the traditional way outlined above.
- No need to rely on having
periodicmethods in every place you use them. - It is easier to create IO class implementations, and you can't accidentally forget to update some input, like in the
updateInputsmethod. - No separation for two objects
ioandinputsfor the getters and setters of the IO, everything is inside theioobject with a clear distinction of fields and output methods. - You don't have to create a "dummy" implementation for a replay that will have a blank
updateInputsmethod, as fields will only be updated by their given supplier if not in a replay. - An easy way to organize the outputs with the same structure as the inputs.
- Overall, less boilerplate code and subjectively cleaner, more readable, maintainable code, with fewer potential bugs.