[epilogue] Improve support for robot base classes in epilogue (#8886)

Generate an `update` method for any logged Robot class, not just
TimedRobot

Continue to generate a `bind` method for TimedRobot subclasses

Also removes unnecessary import statements for generated loggers, since
they're using fully-qualified names in the generated Epilogue class now.
This commit is contained in:
Sam Carlberg
2026-05-29 00:14:30 -04:00
committed by GitHub
parent 1e12408a7d
commit 4fa5dacfc4
3 changed files with 88 additions and 100 deletions

View File

@@ -382,9 +382,12 @@ public class AnnotationProcessor extends AbstractProcessor {
List<String> loggerClassNames = new ArrayList<>(); List<String> loggerClassNames = new ArrayList<>();
var mainRobotClasses = new ArrayList<TypeElement>(); var mainRobotClasses = new ArrayList<TypeElement>();
var timedRobotClasses = new ArrayList<TypeElement>();
// Used to check for a main robot class // Used to check for a main robot class
var robotBaseClass = var robotBaseClass =
processingEnv.getElementUtils().getTypeElement("org.wpilib.framework.RobotBase").asType();
var timedRobotClass =
processingEnv.getElementUtils().getTypeElement("org.wpilib.framework.TimedRobot").asType(); processingEnv.getElementUtils().getTypeElement("org.wpilib.framework.TimedRobot").asType();
boolean validFields = validateFields(annotatedElements); boolean validFields = validateFields(annotatedElements);
@@ -403,6 +406,9 @@ public class AnnotationProcessor extends AbstractProcessor {
if (processingEnv.getTypeUtils().isAssignable(clazz.getSuperclass(), robotBaseClass)) { if (processingEnv.getTypeUtils().isAssignable(clazz.getSuperclass(), robotBaseClass)) {
mainRobotClasses.add(clazz); mainRobotClasses.add(clazz);
} }
if (processingEnv.getTypeUtils().isAssignable(clazz.getSuperclass(), timedRobotClass)) {
timedRobotClasses.add(clazz);
}
loggerClassNames.add(StringUtils.loggerClassName(clazz)); loggerClassNames.add(StringUtils.loggerClassName(clazz));
} catch (IOException e) { } catch (IOException e) {
@@ -417,8 +423,10 @@ public class AnnotationProcessor extends AbstractProcessor {
} }
// Sort alphabetically // Sort alphabetically
mainRobotClasses.sort(Comparator.comparing(c -> c.getSimpleName().toString())); loggerClassNames.sort(Comparator.naturalOrder());
m_epilogueGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses); mainRobotClasses.sort(Comparator.comparing(TypeElement::toString));
timedRobotClasses.sort(Comparator.comparing(TypeElement::toString));
m_epilogueGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses, timedRobotClasses);
} }
private void warnOfNonLoggableElements(TypeElement clazz) { private void warnOfNonLoggableElements(TypeElement clazz) {

View File

@@ -38,12 +38,16 @@ public class EpilogueGenerator {
* *
* @param loggerClassNames the names of the generated logger classes. Each of these will be * @param loggerClassNames the names of the generated logger classes. Each of these will be
* instantiated in a public static field on the Epilogue class. * instantiated in a public static field on the Epilogue class.
* @param mainRobotClasses the main robot classes. May be empty. Used to generate a {@code bind()} * @param mainRobotClasses the main robot classes. May be empty. Used to generate a {@code
* method to add a callback hook to a TimedRobot to log itself. * update()} method for an easy entry point.
* @param timedRobotClasses the main robot classes that extend from TimedRobot. Used to generate a
* {@code bind()} method to add a callback hook to a TimedRobot to log itself.
*/ */
@SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit @SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit
public void writeEpilogueFile( public void writeEpilogueFile(
List<String> loggerClassNames, Collection<TypeElement> mainRobotClasses) { List<String> loggerClassNames,
Collection<TypeElement> mainRobotClasses,
Collection<TypeElement> timedRobotClasses) {
try { try {
var centralStore = var centralStore =
m_processingEnv.getFiler().createSourceFile("org.wpilib.epilogue.Epilogue"); m_processingEnv.getFiler().createSourceFile("org.wpilib.epilogue.Epilogue");
@@ -58,30 +62,6 @@ public class EpilogueGenerator {
out.println("import org.wpilib.hardware.hal.HAL;"); out.println("import org.wpilib.hardware.hal.HAL;");
out.println(); out.println();
loggerClassNames.stream()
.sorted()
.forEach(
name -> {
if (!name.contains(".")) {
// Logger is in the global namespace, don't need to import
return;
}
out.println("import " + name + ";");
});
m_customLoggers.values().stream()
.distinct()
.forEach(
loggerType -> {
var name = loggerType.asElement().toString();
if (!name.contains(".")) {
// Logger is in the global namespace, don't need to import
return;
}
out.println("import " + name + ";");
});
out.println();
out.println("public final class Epilogue {"); out.println("public final class Epilogue {");
// Usage reporting // Usage reporting
@@ -136,56 +116,57 @@ public class EpilogueGenerator {
""" """
.stripTrailing()); .stripTrailing());
// Only generate a binding if the robot class is a TimedRobot for (TypeElement mainRobotClass : mainRobotClasses) {
if (!mainRobotClasses.isEmpty()) { String robotClassName = mainRobotClass.getQualifiedName().toString();
for (TypeElement mainRobotClass : mainRobotClasses) {
String robotClassName = mainRobotClass.getQualifiedName().toString();
out.println(); out.println();
out.print( out.print(
""" """
/** /**
* Updates Epilogue. This must be called periodically in order for Epilogue to record * Updates Epilogue. This must be called periodically in order for Epilogue to record
* new values. Alternatively, {@code bind()} can be used to update at an offset from * new values. Alternatively, {@code bind()} can be used to update at an offset from
* the main robot loop. * the main robot loop.
*/ */
"""); """);
out.println(" public static void update(" + robotClassName + " robot) {"); out.println(" public static void update(" + robotClassName + " robot) {");
out.println(" long start = System.nanoTime();"); out.println(" long start = System.nanoTime();");
out.println( out.println(
" " " "
+ StringUtils.loggerFieldName(mainRobotClass) + StringUtils.loggerFieldName(mainRobotClass)
+ ".tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);"); + ".tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);");
out.println( out.println(
" config.backend.log(\"Epilogue/Stats/Last Run\", (System.nanoTime() - start) / 1e6);"); " config.backend.log(\"Epilogue/Stats/Last Run\", (System.nanoTime() - start) / 1e6);");
out.println(" }"); out.println(" }");
}
out.println(); for (TypeElement timedRobotClass : timedRobotClasses) {
out.print( String robotClassName = timedRobotClass.getQualifiedName().toString();
"""
/** out.println();
* Binds Epilogue updates to a timed robot's update period. Log calls will be made at the out.print(
* same update rate as the robot's loop function, but will be offset by a full phase """
* (for example, a 20ms update rate but 10ms offset from the main loop invocation) to /**
* help avoid high CPU loads. However, this does mean that any logged data that reads * Binds Epilogue updates to a timed robot's update period. Log calls will be made at the
* directly from sensors will be slightly different from data used in the main robot * same update rate as the robot's loop function, but will be offset by a full phase
* loop. * (for example, a 20ms update rate but 10ms offset from the main loop invocation) to
*/ * help avoid high CPU loads. However, this does mean that any logged data that reads
"""); * directly from sensors will be slightly different from data used in the main robot
out.println(" public static void bind(" + robotClassName + " robot) {"); * loop.
out.println(" if (config.loggingPeriod == null) {"); */
out.println(" config.loggingPeriod = Seconds.of(robot.getPeriod());"); """);
out.println(" }"); out.println(" public static void bind(" + robotClassName + " robot) {");
out.println(" if (config.loggingPeriodOffset == null) {"); out.println(" if (config.loggingPeriod == null) {");
out.println(" config.loggingPeriodOffset = config.loggingPeriod.div(2);"); out.println(" config.loggingPeriod = Seconds.of(robot.getPeriod());");
out.println(" }"); out.println(" }");
out.println(); out.println(" if (config.loggingPeriodOffset == null) {");
out.println(" robot.addPeriodic(() -> {"); out.println(" config.loggingPeriodOffset = config.loggingPeriod.div(2);");
out.println(" update(robot);"); out.println(" }");
out.println( out.println();
" }, config.loggingPeriod.in(Seconds), config.loggingPeriodOffset.in(Seconds));"); out.println(" robot.addPeriodic(() -> {");
out.println(" }"); out.println(" update(robot);");
} out.println(
" }, config.loggingPeriod.in(Seconds), config.loggingPeriodOffset.in(Seconds));");
out.println(" }");
} }
out.println("}"); out.println("}");

View File

@@ -35,8 +35,6 @@ class EpilogueGeneratorTest {
import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HAL;
import org.wpilib.epilogue.ExampleLogger;
public final class Epilogue { public final class Epilogue {
static { static {
HAL.reportUsage("Epilogue", ""); HAL.reportUsage("Epilogue", "");
@@ -90,8 +88,6 @@ class EpilogueGeneratorTest {
import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HAL;
import org.wpilib.epilogue.ExampleLogger;
public final class Epilogue { public final class Epilogue {
static { static {
HAL.reportUsage("Epilogue", ""); HAL.reportUsage("Epilogue", "");
@@ -115,6 +111,17 @@ class EpilogueGeneratorTest {
public static boolean shouldLog(Logged.Importance importance) { public static boolean shouldLog(Logged.Importance importance) {
return importance.compareTo(config.minimumImportance) >= 0; return importance.compareTo(config.minimumImportance) >= 0;
} }
/**
* Updates Epilogue. This must be called periodically in order for Epilogue to record
* new values. Alternatively, {@code bind()} can be used to update at an offset from
* the main robot loop.
*/
public static void update(org.wpilib.epilogue.Example robot) {
long start = System.nanoTime();
org_wpilib_epilogue_ExampleLogger.tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);
config.backend.log("Epilogue/Stats/Last Run", (System.nanoTime() - start) / 1e6);
}
} }
"""; """;
@@ -140,8 +147,6 @@ class EpilogueGeneratorTest {
import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HAL;
import org.wpilib.epilogue.ExampleLogger;
public final class Epilogue { public final class Epilogue {
static { static {
HAL.reportUsage("Epilogue", ""); HAL.reportUsage("Epilogue", "");
@@ -224,9 +229,6 @@ class EpilogueGeneratorTest {
import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HAL;
import org.wpilib.epilogue.AlphaBotLogger;
import org.wpilib.epilogue.BetaBotLogger;
public final class Epilogue { public final class Epilogue {
static { static {
HAL.reportUsage("Epilogue", ""); HAL.reportUsage("Epilogue", "");
@@ -263,6 +265,17 @@ class EpilogueGeneratorTest {
config.backend.log("Epilogue/Stats/Last Run", (System.nanoTime() - start) / 1e6); config.backend.log("Epilogue/Stats/Last Run", (System.nanoTime() - start) / 1e6);
} }
/**
* Updates Epilogue. This must be called periodically in order for Epilogue to record
* new values. Alternatively, {@code bind()} can be used to update at an offset from
* the main robot loop.
*/
public static void update(org.wpilib.epilogue.BetaBot robot) {
long start = System.nanoTime();
org_wpilib_epilogue_BetaBotLogger.tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);
config.backend.log("Epilogue/Stats/Last Run", (System.nanoTime() - start) / 1e6);
}
/** /**
* Binds Epilogue updates to a timed robot's update period. Log calls will be made at the * Binds Epilogue updates to a timed robot's update period. Log calls will be made at the
* same update rate as the robot's loop function, but will be offset by a full phase * same update rate as the robot's loop function, but will be offset by a full phase
@@ -284,17 +297,6 @@ class EpilogueGeneratorTest {
}, config.loggingPeriod.in(Seconds), config.loggingPeriodOffset.in(Seconds)); }, config.loggingPeriod.in(Seconds), config.loggingPeriodOffset.in(Seconds));
} }
/**
* Updates Epilogue. This must be called periodically in order for Epilogue to record
* new values. Alternatively, {@code bind()} can be used to update at an offset from
* the main robot loop.
*/
public static void update(org.wpilib.epilogue.BetaBot robot) {
long start = System.nanoTime();
org_wpilib_epilogue_BetaBotLogger.tryUpdate(config.backend.getNested(config.root), robot, config.errorHandler);
config.backend.log("Epilogue/Stats/Last Run", (System.nanoTime() - start) / 1e6);
}
/** /**
* Binds Epilogue updates to a timed robot's update period. Log calls will be made at the * Binds Epilogue updates to a timed robot's update period. Log calls will be made at the
* same update rate as the robot's loop function, but will be offset by a full phase * same update rate as the robot's loop function, but will be offset by a full phase
@@ -357,9 +359,6 @@ class EpilogueGeneratorTest {
import org.wpilib.hardware.hal.HAL; import org.wpilib.hardware.hal.HAL;
import org.wpilib.epilogue.ExampleLogger;
import org.wpilib.epilogue.CustomLogger;
public final class Epilogue { public final class Epilogue {
static { static {
HAL.reportUsage("Epilogue", ""); HAL.reportUsage("Epilogue", "");