mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[epilogue] Add an annotation-based logging framework for Java programs (#6584)
This commit is contained in:
@@ -6,6 +6,7 @@ plugins {
|
||||
evaluationDependsOn(':apriltag')
|
||||
evaluationDependsOn(':cameraserver')
|
||||
evaluationDependsOn(':cscore')
|
||||
evaluationDependsOn(':epilogue-runtime')
|
||||
evaluationDependsOn(':hal')
|
||||
evaluationDependsOn(':ntcore')
|
||||
evaluationDependsOn(':wpilibNewCommands')
|
||||
@@ -234,6 +235,7 @@ task generateJavaDocs(type: Javadoc) {
|
||||
source project(':apriltag').sourceSets.main.java
|
||||
source project(':cameraserver').sourceSets.main.java
|
||||
source project(':cscore').sourceSets.main.java
|
||||
source project(':epilogue-runtime').sourceSets.main.java
|
||||
source project(':hal').sourceSets.main.java
|
||||
source project(':ntcore').sourceSets.main.java
|
||||
source project(':wpilibNewCommands').sourceSets.main.java
|
||||
|
||||
19
epilogue-processor/build.gradle
Normal file
19
epilogue-processor/build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
ext {
|
||||
useJava = true
|
||||
useCpp = false
|
||||
baseId = 'epilogue-processor'
|
||||
groupId = 'edu.wpi.first.epilogue'
|
||||
|
||||
devMain = ''
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/java/javacommon.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':epilogue-runtime'))
|
||||
api project(':wpilibNewCommands')
|
||||
|
||||
implementation 'com.google.auto.service:auto-service:1.1.1'
|
||||
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
|
||||
testImplementation 'com.google.testing.compile:compile-testing:+'
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import edu.wpi.first.epilogue.CustomLoggerFor;
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.NotLogged;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.Processor;
|
||||
import javax.annotation.processing.RoundEnvironment;
|
||||
import javax.annotation.processing.SupportedAnnotationTypes;
|
||||
import javax.annotation.processing.SupportedSourceVersion;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.NoType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.tools.Diagnostic;
|
||||
|
||||
@SupportedAnnotationTypes({
|
||||
"edu.wpi.first.epilogue.CustomLoggerFor",
|
||||
"edu.wpi.first.epilogue.Logged"
|
||||
})
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_17)
|
||||
@AutoService(Processor.class)
|
||||
public class AnnotationProcessor extends AbstractProcessor {
|
||||
private static final String kCustomLoggerFqn = "edu.wpi.first.epilogue.CustomLoggerFor";
|
||||
private static final String kClassSpecificLoggerFqn =
|
||||
"edu.wpi.first.epilogue.logging.ClassSpecificLogger";
|
||||
private static final String kLoggedFqn = "edu.wpi.first.epilogue.Logged";
|
||||
|
||||
private EpilogueGenerator m_epiloguerGenerator;
|
||||
private LoggerGenerator m_loggerGenerator;
|
||||
private List<ElementHandler> m_handlers;
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
if (annotations.isEmpty()) {
|
||||
// Nothing to do, don't claim
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<TypeMirror, DeclaredType> customLoggers = new HashMap<>();
|
||||
|
||||
annotations.stream()
|
||||
.filter(ann -> kCustomLoggerFqn.contentEquals(ann.getQualifiedName()))
|
||||
.findAny()
|
||||
.ifPresent(
|
||||
customLogger -> {
|
||||
customLoggers.putAll(processCustomLoggers(roundEnv, customLogger));
|
||||
});
|
||||
|
||||
roundEnv.getRootElements().stream()
|
||||
.filter(
|
||||
e ->
|
||||
processingEnv
|
||||
.getTypeUtils()
|
||||
.isAssignable(
|
||||
e.asType(),
|
||||
processingEnv
|
||||
.getTypeUtils()
|
||||
.erasure(
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement(kClassSpecificLoggerFqn)
|
||||
.asType())))
|
||||
.filter(e -> e.getAnnotation(CustomLoggerFor.class) == null)
|
||||
.forEach(
|
||||
e -> {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"Custom logger classes should have a @CustomLoggerFor annotation",
|
||||
e);
|
||||
});
|
||||
|
||||
// Handlers are declared in order of priority. If an element could be logged in more than one
|
||||
// way (eg a class implements both Sendable and StructSerializable), the order of the handlers
|
||||
// in this list will determine how it gets logged.
|
||||
m_handlers =
|
||||
List.of(
|
||||
new LoggableHandler(processingEnv), // prioritize epilogue logging over Sendable
|
||||
new ConfiguredLoggerHandler(
|
||||
processingEnv, customLoggers), // then customized logging configs
|
||||
new ArrayHandler(processingEnv),
|
||||
new CollectionHandler(processingEnv),
|
||||
new EnumHandler(processingEnv),
|
||||
new MeasureHandler(processingEnv),
|
||||
new PrimitiveHandler(processingEnv),
|
||||
new SupplierHandler(processingEnv),
|
||||
new StructHandler(processingEnv), // prioritize struct over sendable
|
||||
new SendableHandler(processingEnv));
|
||||
|
||||
m_epiloguerGenerator = new EpilogueGenerator(processingEnv, customLoggers);
|
||||
m_loggerGenerator = new LoggerGenerator(processingEnv, m_handlers);
|
||||
|
||||
annotations.stream()
|
||||
.filter(ann -> kLoggedFqn.contentEquals(ann.getQualifiedName()))
|
||||
.findAny()
|
||||
.ifPresent(
|
||||
epilogue -> {
|
||||
processEpilogue(roundEnv, epilogue);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean validateFields(Set<? extends Element> annotatedElements) {
|
||||
var fields =
|
||||
annotatedElements.stream()
|
||||
.filter(e -> e instanceof VariableElement)
|
||||
.map(e -> (VariableElement) e)
|
||||
.toList();
|
||||
|
||||
boolean valid = true;
|
||||
|
||||
for (VariableElement field : fields) {
|
||||
// Field is explicitly tagged
|
||||
// And is not opted out of
|
||||
if (field.getAnnotation(NotLogged.class) == null && isNotLoggable(field, field.asType())) {
|
||||
// And is not of a loggable type
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"[EPILOGUE] You have opted in to logging on this field, "
|
||||
+ "but it is not a loggable data type!",
|
||||
field);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
private boolean validateMethods(Set<? extends Element> annotatedElements) {
|
||||
var methods =
|
||||
annotatedElements.stream()
|
||||
.filter(e -> e instanceof ExecutableElement)
|
||||
.map(e -> (ExecutableElement) e)
|
||||
.toList();
|
||||
|
||||
boolean valid = true;
|
||||
|
||||
for (ExecutableElement method : methods) {
|
||||
// Field is explicitly tagged
|
||||
if (method.getAnnotation(NotLogged.class) == null) {
|
||||
// And is not opted out of
|
||||
if (isNotLoggable(method, method.getReturnType())) {
|
||||
// And is not of a loggable type
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"[EPILOGUE] You have opted in to logging on this method, "
|
||||
+ "but it does not return a loggable data type!",
|
||||
method);
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
|
||||
// Only public methods can be logged
|
||||
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods must be public", method);
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (method.getModifiers().contains(Modifier.STATIC)) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods cannot be static", method);
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (method.getReturnType().getKind() == TypeKind.NONE) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods cannot be void", method);
|
||||
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!method.getParameters().isEmpty()) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"[EPILOGUE] Logged methods cannot accept arguments",
|
||||
method);
|
||||
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a type is not loggable.
|
||||
*
|
||||
* @param type the type to check
|
||||
*/
|
||||
private boolean isNotLoggable(Element element, TypeMirror type) {
|
||||
if (type instanceof NoType) {
|
||||
// e.g. void, cannot log
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean loggable = m_handlers.stream().anyMatch(h -> h.isLoggable(element));
|
||||
|
||||
if (loggable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.NOTE,
|
||||
"[EPILOGUE] Excluded from logs because " + type + " is not a loggable data type",
|
||||
element);
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<DeclaredType, DeclaredType> processCustomLoggers(
|
||||
RoundEnvironment roundEnv, TypeElement customLoggerAnnotation) {
|
||||
// map logged type to its custom logger, eg
|
||||
// { Point.class => CustomPointLogger.class }
|
||||
var customLoggers = new HashMap<DeclaredType, DeclaredType>();
|
||||
|
||||
var annotatedElements = roundEnv.getElementsAnnotatedWith(customLoggerAnnotation);
|
||||
|
||||
var loggerSuperClass =
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement("edu.wpi.first.epilogue.logging.ClassSpecificLogger");
|
||||
|
||||
for (Element annotatedElement : annotatedElements) {
|
||||
List<AnnotationValue> targetTypes = List.of();
|
||||
for (AnnotationMirror annotationMirror : annotatedElement.getAnnotationMirrors()) {
|
||||
for (var entry : annotationMirror.getElementValues().entrySet()) {
|
||||
if ("value".equals(entry.getKey().getSimpleName().toString())) {
|
||||
targetTypes = (List<AnnotationValue>) entry.getValue().getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasPublicNoArgConstructor =
|
||||
annotatedElement.getEnclosedElements().stream()
|
||||
.anyMatch(
|
||||
enclosedElement ->
|
||||
enclosedElement instanceof ExecutableElement exe
|
||||
&& exe.getKind() == ElementKind.CONSTRUCTOR
|
||||
&& exe.getModifiers().contains(Modifier.PUBLIC)
|
||||
&& exe.getParameters().isEmpty());
|
||||
|
||||
if (!hasPublicNoArgConstructor) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"Logger classes must have a public no-argument constructor",
|
||||
annotatedElement);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (AnnotationValue value : targetTypes) {
|
||||
var targetType = (DeclaredType) value.getValue();
|
||||
var reflectedTarget = targetType.asElement();
|
||||
|
||||
// eg ClassSpecificLogger<MyDataType>
|
||||
var requiredSuperClass =
|
||||
processingEnv
|
||||
.getTypeUtils()
|
||||
.getDeclaredType(
|
||||
loggerSuperClass,
|
||||
processingEnv.getTypeUtils().getWildcardType(null, reflectedTarget.asType()));
|
||||
|
||||
if (customLoggers.containsKey(targetType)) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"Multiple custom loggers detected for type " + targetType,
|
||||
annotatedElement);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!processingEnv
|
||||
.getTypeUtils()
|
||||
.isAssignable(annotatedElement.asType(), requiredSuperClass)) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"Not a subclass of ClassSpecificLogger<" + targetType + ">",
|
||||
annotatedElement);
|
||||
continue;
|
||||
}
|
||||
|
||||
customLoggers.put(targetType, (DeclaredType) annotatedElement.asType());
|
||||
}
|
||||
}
|
||||
|
||||
return customLoggers;
|
||||
}
|
||||
|
||||
private void processEpilogue(RoundEnvironment roundEnv, TypeElement epilogueAnnotation) {
|
||||
var annotatedElements = roundEnv.getElementsAnnotatedWith(epilogueAnnotation);
|
||||
|
||||
List<String> loggerClassNames = new ArrayList<>();
|
||||
var mainRobotClasses = new ArrayList<TypeElement>();
|
||||
|
||||
// Used to check for a main robot class
|
||||
var robotBaseClass =
|
||||
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.wpilibj.TimedRobot").asType();
|
||||
|
||||
boolean validFields = validateFields(annotatedElements);
|
||||
boolean validMethods = validateMethods(annotatedElements);
|
||||
|
||||
if (!(validFields && validMethods)) {
|
||||
// Generate nothing and bail
|
||||
return;
|
||||
}
|
||||
|
||||
var classes =
|
||||
annotatedElements.stream()
|
||||
.filter(e -> e instanceof TypeElement)
|
||||
.map(e -> (TypeElement) e)
|
||||
.toList();
|
||||
for (TypeElement clazz : classes) {
|
||||
try {
|
||||
warnOfNonLoggableElements(clazz);
|
||||
m_loggerGenerator.writeLoggerFile(clazz);
|
||||
|
||||
if (processingEnv.getTypeUtils().isAssignable(clazz.getSuperclass(), robotBaseClass)) {
|
||||
mainRobotClasses.add(clazz);
|
||||
}
|
||||
|
||||
loggerClassNames.add(StringUtils.loggerClassName(clazz));
|
||||
} catch (IOException e) {
|
||||
processingEnv
|
||||
.getMessager()
|
||||
.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"Could not write logger file for " + clazz.getQualifiedName(),
|
||||
clazz);
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
mainRobotClasses.sort(Comparator.comparing(c -> c.getSimpleName().toString()));
|
||||
m_epiloguerGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses);
|
||||
}
|
||||
|
||||
private void warnOfNonLoggableElements(TypeElement clazz) {
|
||||
var config = clazz.getAnnotation(Logged.class);
|
||||
if (config.strategy() == Logged.Strategy.OPT_IN) {
|
||||
// field and method validations will have already checked everything
|
||||
return;
|
||||
}
|
||||
|
||||
for (Element element : clazz.getEnclosedElements()) {
|
||||
if (element.getAnnotation(NotLogged.class) != null) {
|
||||
// Explicitly opted out from, don't need to check
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.getModifiers().contains(Modifier.STATIC)) {
|
||||
// static elements are never logged
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element instanceof VariableElement v) {
|
||||
// isNotLoggable will internally print a warning message
|
||||
isNotLoggable(v, v.asType());
|
||||
}
|
||||
|
||||
if (element instanceof ExecutableElement exe
|
||||
&& exe.getModifiers().contains(Modifier.PUBLIC)
|
||||
&& exe.getParameters().isEmpty()) {
|
||||
// isNotLoggable will internally print a warning message
|
||||
isNotLoggable(exe, exe.getReturnType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.PrimitiveType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* Arrays of bytes, ints, flats, doubles, booleans, Strings, and struct-serializable objects can be
|
||||
* logged. No other array types - including multidimensional arrays - are loggable.
|
||||
*/
|
||||
public class ArrayHandler extends ElementHandler {
|
||||
private final StructHandler m_structHandler;
|
||||
private final TypeMirror m_javaLangString;
|
||||
|
||||
protected ArrayHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
// use a struct handler for managing struct arrays
|
||||
m_structHandler = new StructHandler(processingEnv);
|
||||
|
||||
m_javaLangString = lookupTypeElement(processingEnv, "java.lang.String").asType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return dataType(element) instanceof ArrayType arr
|
||||
&& isLoggableComponentType(arr.getComponentType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an array containing elements of the given type can be logged.
|
||||
*
|
||||
* @param type the data type to check
|
||||
* @return true if an array like {@code type[]} can be logged, false otherwise
|
||||
*/
|
||||
public boolean isLoggableComponentType(TypeMirror type) {
|
||||
if (type instanceof PrimitiveType primitive) {
|
||||
return switch (primitive.getKind()) {
|
||||
case BYTE, INT, LONG, FLOAT, DOUBLE, BOOLEAN -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
return m_structHandler.isLoggableType(type)
|
||||
|| m_processingEnv.getTypeUtils().isAssignable(type, m_javaLangString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
var dataType = dataType(element);
|
||||
|
||||
// known to be an array type (assuming isLoggable is checked first); this is a safe cast
|
||||
var componentType = ((ArrayType) dataType).getComponentType();
|
||||
|
||||
if (m_structHandler.isLoggableType(componentType)) {
|
||||
// Struct arrays need to pass in the struct serializer
|
||||
return "dataLogger.log(\""
|
||||
+ loggedName(element)
|
||||
+ "\", "
|
||||
+ elementAccess(element)
|
||||
+ ", "
|
||||
+ m_structHandler.structAccess(componentType)
|
||||
+ ")";
|
||||
} else {
|
||||
// Primitive or string array
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* Collections of strings and structs are loggable. Collections of boxed primitive types are not.
|
||||
*/
|
||||
public class CollectionHandler extends ElementHandler {
|
||||
private final ArrayHandler m_arrayHandler;
|
||||
private final TypeMirror m_collectionType;
|
||||
private final StructHandler m_structHandler;
|
||||
|
||||
protected CollectionHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
m_arrayHandler = new ArrayHandler(processingEnv);
|
||||
m_collectionType =
|
||||
processingEnv.getElementUtils().getTypeElement("java.util.Collection").asType();
|
||||
m_structHandler = new StructHandler(processingEnv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
var dataType = dataType(element);
|
||||
|
||||
return m_processingEnv
|
||||
.getTypeUtils()
|
||||
.isAssignable(dataType, m_processingEnv.getTypeUtils().erasure(m_collectionType))
|
||||
&& dataType instanceof DeclaredType decl
|
||||
&& decl.getTypeArguments().size() == 1
|
||||
&& m_arrayHandler.isLoggableComponentType(decl.getTypeArguments().get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
var dataType = dataType(element);
|
||||
var componentType = ((DeclaredType) dataType).getTypeArguments().get(0);
|
||||
|
||||
if (m_structHandler.isLoggableType(componentType)) {
|
||||
return "dataLogger.log(\""
|
||||
+ loggedName(element)
|
||||
+ "\", "
|
||||
+ elementAccess(element)
|
||||
+ ", "
|
||||
+ m_structHandler.structAccess(componentType)
|
||||
+ ")";
|
||||
} else {
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class ConfiguredLoggerHandler extends ElementHandler {
|
||||
private final Map<TypeMirror, DeclaredType> m_customLoggers;
|
||||
|
||||
protected ConfiguredLoggerHandler(
|
||||
ProcessingEnvironment processingEnv, Map<TypeMirror, DeclaredType> customLoggers) {
|
||||
super(processingEnv);
|
||||
|
||||
this.m_customLoggers = customLoggers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_customLoggers.containsKey(dataType(element));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
var dataType = dataType(element);
|
||||
var loggerType = m_customLoggers.get(dataType);
|
||||
|
||||
return "Epilogue."
|
||||
+ StringUtils.lowerCamelCase(loggerType.asElement().getSimpleName())
|
||||
+ ".tryUpdate(dataLogger.getSubLogger(\""
|
||||
+ loggedName(element)
|
||||
+ "\"), "
|
||||
+ elementAccess(element)
|
||||
+ ", Epilogue.getConfig().errorHandler)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
import edu.wpi.first.epilogue.logging.DataLogger;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* Handles logging of fields or methods. An element that passes the {@link #isLoggable(Element)}
|
||||
* check guarantees that {@link #logInvocation(Element)} will generate a code snippet that will log
|
||||
* that element. Some subclasses may return {@code null} for the invocation to signal that the
|
||||
* element should not be logged, but still be considered loggable for the purposes of error
|
||||
* messaging during the compilation phase.
|
||||
*/
|
||||
public abstract class ElementHandler {
|
||||
protected final ProcessingEnvironment m_processingEnv;
|
||||
|
||||
/**
|
||||
* Instantiates the handler.
|
||||
*
|
||||
* @param processingEnv the processing environment, used to look up type information
|
||||
*/
|
||||
protected ElementHandler(ProcessingEnvironment processingEnv) {
|
||||
this.m_processingEnv = processingEnv;
|
||||
}
|
||||
|
||||
protected static TypeElement lookupTypeElement(ProcessingEnvironment processingEnv, String name) {
|
||||
return processingEnv.getElementUtils().getTypeElement(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of data that would be logged by a field or method.
|
||||
*
|
||||
* @param element the field or method element to check
|
||||
* @return the logged datatype
|
||||
*/
|
||||
protected TypeMirror dataType(Element element) {
|
||||
if (element instanceof VariableElement field) {
|
||||
return field.asType();
|
||||
} else if (element instanceof ExecutableElement method) {
|
||||
return method.getReturnType();
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected" + element.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of a field or method as it would appear in logs.
|
||||
*
|
||||
* @param element the field or method element to check
|
||||
* @return the name specified in the {@link Logged @Logged} annotation on the element, if present;
|
||||
* otherwise, the field or method's name with no modifications
|
||||
*/
|
||||
public String loggedName(Element element) {
|
||||
var elementName = element.getSimpleName().toString();
|
||||
var config = element.getAnnotation(Logged.class);
|
||||
|
||||
if (config != null && !config.name().isBlank()) {
|
||||
return config.name();
|
||||
} else {
|
||||
return elementName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the code snippet to use to access a field or method on a logged object. Private
|
||||
* fields are accessed via {@link java.lang.invoke.VarHandle VarHandles} and private methods are
|
||||
* accessed via {@link java.lang.invoke.MethodHandle MethodHandles} (note that this requires the
|
||||
* logger file to generate those fields). Because the generated logger files are in the same
|
||||
* package as the logged type, package-private, protected, and public fields and methods are
|
||||
* always accessible using normal field reads and method calls. Values returned by {@code
|
||||
* VarHandle} and {@code MethodHandle} invocations will be cast to the appropriate data type.
|
||||
*
|
||||
* @param element the element to generate the access for
|
||||
* @return the generated access snippet
|
||||
*/
|
||||
public String elementAccess(Element element) {
|
||||
if (element instanceof VariableElement field) {
|
||||
return fieldAccess(field);
|
||||
} else if (element instanceof ExecutableElement method) {
|
||||
return methodAccess(method);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected" + element.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static String fieldAccess(VariableElement field) {
|
||||
if (field.getModifiers().contains(Modifier.PRIVATE)) {
|
||||
// (com.example.Foo) $fooField.get(object)
|
||||
return "(" + field.asType() + ") $" + field.getSimpleName() + ".get(object)";
|
||||
} else {
|
||||
// object.fooField
|
||||
return "object." + field.getSimpleName();
|
||||
}
|
||||
}
|
||||
|
||||
private static String methodAccess(ExecutableElement method) {
|
||||
if (method.getModifiers().contains(Modifier.PRIVATE)) {
|
||||
// (com.example.Foo) _getFoo.invoke(object)
|
||||
// NOTE: Currently, only public methods are logged, so this branch will not be used
|
||||
return "(" + method.getReturnType() + ") _" + method.getSimpleName() + ".invoke(object)";
|
||||
} else {
|
||||
// object.getFoo()
|
||||
return "object." + method.getSimpleName() + "()";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field or method can be logged by this handler.
|
||||
*
|
||||
* @param element the field or method element to check
|
||||
* @return true if the element can be logged, false if not
|
||||
*/
|
||||
public abstract boolean isLoggable(Element element);
|
||||
|
||||
/**
|
||||
* Generates a code snippet to place in a generated logger file to log the value of a field or
|
||||
* method. Log invocations are placed in a generated implementation of {@link
|
||||
* ClassSpecificLogger#update(DataLogger, Object)}, with access to the data logger and logged
|
||||
* object passed to the method call.
|
||||
*
|
||||
* @param element the field or method element to generate the logger call for
|
||||
* @return the generated log invocation
|
||||
*/
|
||||
public abstract String logInvocation(Element element);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class EnumHandler extends ElementHandler {
|
||||
private final TypeMirror m_javaLangEnum;
|
||||
|
||||
protected EnumHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
// raw type java.lang.Enum
|
||||
m_javaLangEnum =
|
||||
processingEnv
|
||||
.getTypeUtils()
|
||||
.erasure(processingEnv.getElementUtils().getTypeElement("java.lang.Enum").asType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_javaLangEnum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import edu.wpi.first.epilogue.EpilogueConfiguration;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* Generates the {@code Epilogue} file used as the main entry point to logging with Epilogue in a
|
||||
* robot program. {@code Epilogue} has instances of every generated logger class, a {@link
|
||||
* EpilogueConfiguration config} object, and (if the main robot class inherits from {@link
|
||||
* edu.wpi.first.wpilibj.TimedRobot TimedRobot}) a {@code bind()} method to automatically add a
|
||||
* periodic logging call to the robot.
|
||||
*/
|
||||
public class EpilogueGenerator {
|
||||
private final ProcessingEnvironment m_processingEnv;
|
||||
private final Map<TypeMirror, DeclaredType> m_customLoggers;
|
||||
|
||||
public EpilogueGenerator(
|
||||
ProcessingEnvironment processingEnv, Map<TypeMirror, DeclaredType> customLoggers) {
|
||||
this.m_processingEnv = processingEnv;
|
||||
this.m_customLoggers = customLoggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Epilogue file, which is the main entry point for users to set up and interact with
|
||||
* the generated loggers.
|
||||
*
|
||||
* @param loggerClassNames the names of the generated logger classes. Each of these will be
|
||||
* 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()}
|
||||
* method to add a callback hook to a TimedRobot to log itself.
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit
|
||||
public void writeEpilogueFile(
|
||||
List<String> loggerClassNames, Collection<TypeElement> mainRobotClasses) {
|
||||
try {
|
||||
var centralStore =
|
||||
m_processingEnv.getFiler().createSourceFile("edu.wpi.first.epilogue.Epilogue");
|
||||
|
||||
try (var out = new PrintWriter(centralStore.openOutputStream())) {
|
||||
out.println("package edu.wpi.first.epilogue;");
|
||||
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(
|
||||
" private static final EpilogueConfiguration config = new EpilogueConfiguration();");
|
||||
out.println();
|
||||
|
||||
loggerClassNames.forEach(
|
||||
name -> {
|
||||
String simple = StringUtils.simpleName(name);
|
||||
|
||||
// public static final FooLogger fooLogger = new FooLogger();
|
||||
out.print(" public static final ");
|
||||
out.print(simple);
|
||||
out.print(" ");
|
||||
out.print(StringUtils.lowerCamelCase(simple));
|
||||
out.print(" = new ");
|
||||
out.print(simple);
|
||||
out.println("();");
|
||||
});
|
||||
m_customLoggers.values().stream()
|
||||
.distinct()
|
||||
.forEach(
|
||||
loggerType -> {
|
||||
var loggerTypeName = loggerType.asElement().getSimpleName();
|
||||
out.println(
|
||||
" public static final "
|
||||
+ loggerTypeName
|
||||
+ " "
|
||||
+ StringUtils.lowerCamelCase(loggerTypeName)
|
||||
+ " = new "
|
||||
+ loggerTypeName
|
||||
+ "();");
|
||||
});
|
||||
out.println();
|
||||
|
||||
out.println(
|
||||
"""
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
""");
|
||||
|
||||
out.println(
|
||||
"""
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
"""
|
||||
.stripTrailing());
|
||||
|
||||
// Only generate a binding if the robot class is a TimedRobot
|
||||
if (!mainRobotClasses.isEmpty()) {
|
||||
for (TypeElement mainRobotClass : mainRobotClasses) {
|
||||
String robotClassName = mainRobotClass.getQualifiedName().toString();
|
||||
|
||||
out.println();
|
||||
out.print(
|
||||
"""
|
||||
/**
|
||||
* 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
|
||||
* (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
|
||||
* loop.
|
||||
*/
|
||||
""");
|
||||
out.println(" public static void bind(" + robotClassName + " robot) {");
|
||||
out.println(" robot.addPeriodic(() -> {");
|
||||
out.println(" long start = System.nanoTime();");
|
||||
out.println(
|
||||
" "
|
||||
+ StringUtils.loggerFieldName(mainRobotClass)
|
||||
+ ".tryUpdate(config.dataLogger.getSubLogger(config.root), robot, config.errorHandler);");
|
||||
out.println(
|
||||
" edu.wpi.first.networktables.NetworkTableInstance.getDefault().getEntry(\"Epilogue/Stats/Last Run\").setDouble((System.nanoTime() - start) / 1e6);");
|
||||
out.println(" }, robot.getPeriod(), robot.getPeriod() / 2);");
|
||||
out.println(" }");
|
||||
}
|
||||
}
|
||||
|
||||
out.println("}");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/** Handles logging for types annotated with the {@link Logged @Logged} annotation. */
|
||||
public class LoggableHandler extends ElementHandler {
|
||||
protected LoggableHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
var dataType = dataType(element);
|
||||
return dataType.getAnnotation(Logged.class) != null
|
||||
|| dataType instanceof DeclaredType decl
|
||||
&& decl.asElement().getAnnotation(Logged.class) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
TypeMirror dataType = dataType(element);
|
||||
var reflectedType =
|
||||
m_processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement(m_processingEnv.getTypeUtils().erasure(dataType).toString());
|
||||
|
||||
return "Epilogue."
|
||||
+ StringUtils.loggerFieldName(reflectedType)
|
||||
+ ".tryUpdate(dataLogger.getSubLogger(\""
|
||||
+ loggedName(element)
|
||||
+ "\"), "
|
||||
+ elementAccess(element)
|
||||
+ ", Epilogue.getConfig().errorHandler)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.NotLogged;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
|
||||
/** Generates logger class files for {@link Logged @Logged}-annotated classes. */
|
||||
public class LoggerGenerator {
|
||||
private final ProcessingEnvironment m_processingEnv;
|
||||
private final List<ElementHandler> m_handlers;
|
||||
|
||||
public LoggerGenerator(ProcessingEnvironment processingEnv, List<ElementHandler> handlers) {
|
||||
this.m_processingEnv = processingEnv;
|
||||
this.m_handlers = handlers;
|
||||
}
|
||||
|
||||
private static boolean isNotSkipped(Element e) {
|
||||
return e.getAnnotation(NotLogged.class) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the logger class used to handle data objects of the given type. The generated logger
|
||||
* class will subclass from {@link edu.wpi.first.epilogue.logging.ClassSpecificLogger} and
|
||||
* implement the {@code update()} method to populate a data log with information from an instance
|
||||
* of the data type.
|
||||
*
|
||||
* @param clazz the data type that the logger should support.
|
||||
* @throws IOException if the file could not be written
|
||||
*/
|
||||
public void writeLoggerFile(TypeElement clazz) throws IOException {
|
||||
var config = clazz.getAnnotation(Logged.class);
|
||||
boolean requireExplicitOptIn = config.strategy() == Logged.Strategy.OPT_IN;
|
||||
|
||||
Predicate<Element> notSkipped = LoggerGenerator::isNotSkipped;
|
||||
Predicate<Element> optedIn =
|
||||
e -> !requireExplicitOptIn || e.getAnnotation(Logged.class) != null;
|
||||
|
||||
var fieldsToLog =
|
||||
clazz.getEnclosedElements().stream()
|
||||
.filter(e -> e instanceof VariableElement)
|
||||
.map(e -> (VariableElement) e)
|
||||
.filter(notSkipped)
|
||||
.filter(optedIn)
|
||||
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(this::isLoggable)
|
||||
.toList();
|
||||
|
||||
var methodsToLog =
|
||||
clazz.getEnclosedElements().stream()
|
||||
.filter(e -> e instanceof ExecutableElement)
|
||||
.map(e -> (ExecutableElement) e)
|
||||
.filter(notSkipped)
|
||||
.filter(optedIn)
|
||||
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
|
||||
.filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
|
||||
.filter(e -> e.getParameters().isEmpty())
|
||||
.filter(e -> e.getReceiverType() != null)
|
||||
.filter(this::isLoggable)
|
||||
.toList();
|
||||
|
||||
writeLoggerFile(clazz.getQualifiedName().toString(), config, fieldsToLog, methodsToLog);
|
||||
}
|
||||
|
||||
private void writeLoggerFile(
|
||||
String className,
|
||||
Logged classConfig,
|
||||
List<VariableElement> loggableFields,
|
||||
List<ExecutableElement> loggableMethods)
|
||||
throws IOException {
|
||||
String packageName = null;
|
||||
int lastDot = className.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
packageName = className.substring(0, lastDot);
|
||||
}
|
||||
|
||||
String simpleClassName = StringUtils.simpleName(className);
|
||||
String loggerClassName = className + "Logger";
|
||||
String loggerSimpleClassName = loggerClassName.substring(lastDot + 1);
|
||||
|
||||
// Use the name on the class config to set the generated logger names
|
||||
// This helps to avoid naming conflicts
|
||||
if (!classConfig.name().isBlank()) {
|
||||
loggerSimpleClassName =
|
||||
StringUtils.capitalize(classConfig.name().replaceAll(" ", "")) + "Logger";
|
||||
if (lastDot > 0) {
|
||||
loggerClassName = packageName + "." + loggerSimpleClassName;
|
||||
} else {
|
||||
loggerClassName = loggerSimpleClassName;
|
||||
}
|
||||
}
|
||||
|
||||
var loggerFile = m_processingEnv.getFiler().createSourceFile(loggerClassName);
|
||||
|
||||
var privateFields =
|
||||
loggableFields.stream().filter(e -> e.getModifiers().contains(Modifier.PRIVATE)).toList();
|
||||
boolean requiresVarHandles = !privateFields.isEmpty();
|
||||
|
||||
try (var out = new PrintWriter(loggerFile.openWriter())) {
|
||||
if (packageName != null) {
|
||||
// package com.example;
|
||||
out.println("package " + packageName + ";");
|
||||
out.println();
|
||||
}
|
||||
|
||||
out.println("import edu.wpi.first.epilogue.Logged;");
|
||||
out.println("import edu.wpi.first.epilogue.Epilogue;");
|
||||
out.println("import edu.wpi.first.epilogue.logging.ClassSpecificLogger;");
|
||||
out.println("import edu.wpi.first.epilogue.logging.DataLogger;");
|
||||
if (requiresVarHandles) {
|
||||
out.println("import java.lang.invoke.MethodHandles;");
|
||||
out.println("import java.lang.invoke.VarHandle;");
|
||||
}
|
||||
out.println();
|
||||
|
||||
// public class FooLogger implements ClassSpecificLogger<Foo> {
|
||||
out.println(
|
||||
"public class "
|
||||
+ loggerSimpleClassName
|
||||
+ " extends ClassSpecificLogger<"
|
||||
+ simpleClassName
|
||||
+ "> {");
|
||||
|
||||
if (requiresVarHandles) {
|
||||
for (var privateField : privateFields) {
|
||||
// This field needs a VarHandle to access.
|
||||
// Cache it in the class to avoid lookups
|
||||
out.println(" private static final VarHandle $" + privateField.getSimpleName() + ";");
|
||||
}
|
||||
out.println();
|
||||
|
||||
var clazz = simpleClassName + ".class";
|
||||
|
||||
out.println(" static {");
|
||||
out.println(" try {");
|
||||
out.println(
|
||||
" var lookup = MethodHandles.privateLookupIn("
|
||||
+ clazz
|
||||
+ ", MethodHandles.lookup());");
|
||||
|
||||
for (var privateField : privateFields) {
|
||||
var fieldName = privateField.getSimpleName();
|
||||
out.println(
|
||||
" $"
|
||||
+ fieldName
|
||||
+ " = lookup.findVarHandle("
|
||||
+ clazz
|
||||
+ ", \""
|
||||
+ fieldName
|
||||
+ "\", "
|
||||
+ m_processingEnv.getTypeUtils().erasure(privateField.asType())
|
||||
+ ".class);");
|
||||
}
|
||||
|
||||
out.println(" } catch (ReflectiveOperationException e) {");
|
||||
out.println(
|
||||
" throw new RuntimeException("
|
||||
+ "\"[EPILOGUE] Could not load private fields for logging!\", e);");
|
||||
out.println(" }");
|
||||
out.println(" }");
|
||||
out.println();
|
||||
}
|
||||
|
||||
out.println(" public " + loggerSimpleClassName + "() {");
|
||||
out.println(" super(" + simpleClassName + ".class);");
|
||||
out.println(" }");
|
||||
out.println();
|
||||
|
||||
// @Override
|
||||
// public void update(DataLogger dataLogger, Foo object) {
|
||||
out.println(" @Override");
|
||||
out.println(" public void update(DataLogger dataLogger, " + simpleClassName + " object) {");
|
||||
|
||||
// Build a map of importance levels to the fields logged at those levels
|
||||
// e.g. { DEBUG: [fieldA, fieldB], INFO: [fieldC], CRITICAL: [fieldD, fieldE, fieldF] }
|
||||
var loggedElementsByImportance =
|
||||
Stream.concat(loggableFields.stream(), loggableMethods.stream())
|
||||
.collect(
|
||||
groupingBy(
|
||||
element -> {
|
||||
var config = element.getAnnotation(Logged.class);
|
||||
if (config == null) {
|
||||
// No configuration on this element, fall back to the class-level
|
||||
// configuration
|
||||
return classConfig.importance();
|
||||
} else {
|
||||
return config.importance();
|
||||
}
|
||||
},
|
||||
() ->
|
||||
new EnumMap<>(Logged.Importance.class), // EnumMap for consistent ordering
|
||||
toList()));
|
||||
|
||||
loggedElementsByImportance.forEach(
|
||||
(importance, elements) -> {
|
||||
out.println(
|
||||
" if (Epilogue.shouldLog(Logged.Importance." + importance.name() + ")) {");
|
||||
|
||||
for (var loggableElement : elements) {
|
||||
// findFirst for prioritization
|
||||
var handler =
|
||||
m_handlers.stream().filter(h -> h.isLoggable(loggableElement)).findFirst();
|
||||
|
||||
handler.ifPresent(
|
||||
h -> {
|
||||
// May be null if the handler consumes the element but does not actually want it
|
||||
// to be logged. For example, the sendable handler consumes all sendable types
|
||||
// but does not log commands or subsystems, to prevent excessive warnings about
|
||||
// unloggable commands.
|
||||
var logInvocation = h.logInvocation(loggableElement);
|
||||
if (logInvocation != null) {
|
||||
out.println(logInvocation.indent(6).stripTrailing() + ";");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
out.println(" }");
|
||||
});
|
||||
|
||||
out.println(" }");
|
||||
out.println("}");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLoggable(Element element) {
|
||||
return m_handlers.stream().anyMatch(h -> h.isLoggable(element));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class MeasureHandler extends ElementHandler {
|
||||
private final TypeMirror m_measure;
|
||||
|
||||
protected MeasureHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
m_measure =
|
||||
processingEnv
|
||||
.getTypeUtils()
|
||||
.erasure(
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement("edu.wpi.first.units.Measure")
|
||||
.asType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_measure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
// DataLogger has builtin support for logging measures
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import static javax.lang.model.type.TypeKind.BOOLEAN;
|
||||
import static javax.lang.model.type.TypeKind.BYTE;
|
||||
import static javax.lang.model.type.TypeKind.CHAR;
|
||||
import static javax.lang.model.type.TypeKind.DOUBLE;
|
||||
import static javax.lang.model.type.TypeKind.FLOAT;
|
||||
import static javax.lang.model.type.TypeKind.INT;
|
||||
import static javax.lang.model.type.TypeKind.LONG;
|
||||
import static javax.lang.model.type.TypeKind.SHORT;
|
||||
|
||||
import java.util.Set;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class PrimitiveHandler extends ElementHandler {
|
||||
private final TypeMirror m_javaLangString;
|
||||
|
||||
protected PrimitiveHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
m_javaLangString = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_javaLangString)
|
||||
|| Set.of(BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, BOOLEAN)
|
||||
.contains(dataType(element).getKind());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class SendableHandler extends ElementHandler {
|
||||
private final TypeMirror m_sendableType;
|
||||
private final TypeMirror m_commandType;
|
||||
private final TypeMirror m_subsystemType;
|
||||
|
||||
protected SendableHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
m_sendableType =
|
||||
lookupTypeElement(processingEnv, "edu.wpi.first.util.sendable.Sendable").asType();
|
||||
m_commandType =
|
||||
lookupTypeElement(processingEnv, "edu.wpi.first.wpilibj2.command.Command").asType();
|
||||
m_subsystemType =
|
||||
lookupTypeElement(processingEnv, "edu.wpi.first.wpilibj2.command.SubsystemBase").asType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
var dataType = dataType(element);
|
||||
|
||||
// Accept any sendable type. However, the log invocation will return null
|
||||
// for sendable types that should not be logged (commands, subsystems)
|
||||
return m_processingEnv.getTypeUtils().isAssignable(dataType, m_sendableType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
var dataType = dataType(element);
|
||||
|
||||
if (m_processingEnv.getTypeUtils().isAssignable(dataType, m_commandType)
|
||||
|| m_processingEnv.getTypeUtils().isAssignable(dataType, m_subsystemType)) {
|
||||
// Do not log commands or subsystems via their sendable implementations
|
||||
// We accept all sendable objects to prevent them from being reported as not loggable,
|
||||
// but their sendable implementations do not include helpful information.
|
||||
// Users are free to provide custom logging implementations for commands, and tag their
|
||||
// subsystems with @Logged to log their contents automatically
|
||||
return null;
|
||||
}
|
||||
|
||||
return "logSendable(dataLogger.getSubLogger(\""
|
||||
+ loggedName(element)
|
||||
+ "\"), "
|
||||
+ elementAccess(element)
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
public final class StringUtils {
|
||||
private StringUtils() {
|
||||
throw new UnsupportedOperationException("This is a utility class!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the simple name of a fully qualified class.
|
||||
*
|
||||
* @param fqn the fully qualified class name
|
||||
* @return the simple class name, without a package specifier
|
||||
*/
|
||||
public static String simpleName(String fqn) {
|
||||
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalizes a string. The first character will be capitalized, and the rest of the string will
|
||||
* be left as is.
|
||||
*
|
||||
* @param str the string to capitalize
|
||||
* @return the capitalized string
|
||||
*/
|
||||
public static String capitalize(CharSequence str) {
|
||||
return Character.toUpperCase(str.charAt(0)) + str.subSequence(1, str.length()).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a lower camel-cased version. This requires the input string to only
|
||||
* consist of alphanumeric characters, without underscores, spaces, or any other special
|
||||
* character.
|
||||
*
|
||||
* @param str the string to convert
|
||||
* @return the lower camel-case version of the string
|
||||
*/
|
||||
public static String lowerCamelCase(CharSequence str) {
|
||||
StringBuilder builder = new StringBuilder(str.length());
|
||||
|
||||
int i = 0;
|
||||
for (;
|
||||
i < str.length()
|
||||
&& (i == 0
|
||||
|| i == str.length() - 1
|
||||
|| Character.isUpperCase(str.charAt(i))
|
||||
&& Character.isUpperCase(str.charAt(i + 1)));
|
||||
i++) {
|
||||
builder.append(Character.toLowerCase(str.charAt(i)));
|
||||
}
|
||||
|
||||
builder.append(str.subSequence(i, str.length()));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the field used to hold a logger for data of the given type.
|
||||
*
|
||||
* @param clazz the data type that the logger supports
|
||||
* @return the logger field name
|
||||
*/
|
||||
public static String loggerFieldName(TypeElement clazz) {
|
||||
return lowerCamelCase(simpleName(loggerClassName(clazz)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the generated class used to log data of the given type. This will be
|
||||
* fully-qualified class name, such as {@code "frc.robot.MyRobotLogger"}.
|
||||
*
|
||||
* @param clazz the data type that the logger supports
|
||||
* @return the logger class name
|
||||
*/
|
||||
public static String loggerClassName(TypeElement clazz) {
|
||||
var config = clazz.getAnnotation(Logged.class);
|
||||
var className = clazz.getQualifiedName().toString();
|
||||
|
||||
String packageName;
|
||||
int lastDot = className.lastIndexOf('.');
|
||||
if (lastDot <= 0) {
|
||||
packageName = null;
|
||||
} else {
|
||||
packageName = className.substring(0, lastDot);
|
||||
}
|
||||
|
||||
String loggerClassName;
|
||||
|
||||
// Use the name on the class config to set the generated logger names
|
||||
// This helps to avoid naming conflicts
|
||||
if (config.name().isBlank()) {
|
||||
loggerClassName = className + "Logger";
|
||||
} else {
|
||||
String cleaned = config.name().replaceAll(" ", "");
|
||||
|
||||
var loggerSimpleClassName = StringUtils.capitalize(cleaned) + "Logger";
|
||||
if (packageName != null) {
|
||||
loggerClassName = packageName + "." + loggerSimpleClassName;
|
||||
} else {
|
||||
loggerClassName = loggerSimpleClassName;
|
||||
}
|
||||
}
|
||||
|
||||
return loggerClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.util.Types;
|
||||
|
||||
public class StructHandler extends ElementHandler {
|
||||
private final TypeMirror m_serializable;
|
||||
private final Types m_typeUtils;
|
||||
|
||||
protected StructHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
m_serializable =
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement("edu.wpi.first.util.struct.StructSerializable")
|
||||
.asType();
|
||||
m_typeUtils = processingEnv.getTypeUtils();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_typeUtils.isAssignable(dataType(element), m_serializable);
|
||||
}
|
||||
|
||||
public boolean isLoggableType(TypeMirror type) {
|
||||
return m_typeUtils.isAssignable(type, m_serializable);
|
||||
}
|
||||
|
||||
public String structAccess(TypeMirror serializableType) {
|
||||
var className = m_typeUtils.erasure(serializableType).toString();
|
||||
return className + ".struct";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
return "dataLogger.log(\""
|
||||
+ loggedName(element)
|
||||
+ "\", "
|
||||
+ elementAccess(element)
|
||||
+ ", "
|
||||
+ structAccess(dataType(element))
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
public class SupplierHandler extends ElementHandler {
|
||||
private final TypeMirror m_booleanSupplier;
|
||||
private final TypeMirror m_intSupplier;
|
||||
private final TypeMirror m_longSupplier;
|
||||
private final TypeMirror m_doubleSupplier;
|
||||
|
||||
protected SupplierHandler(ProcessingEnvironment processingEnv) {
|
||||
super(processingEnv);
|
||||
|
||||
m_booleanSupplier =
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement("java.util.function.BooleanSupplier")
|
||||
.asType();
|
||||
m_intSupplier =
|
||||
processingEnv.getElementUtils().getTypeElement("java.util.function.IntSupplier").asType();
|
||||
m_longSupplier =
|
||||
processingEnv.getElementUtils().getTypeElement("java.util.function.LongSupplier").asType();
|
||||
m_doubleSupplier =
|
||||
processingEnv
|
||||
.getElementUtils()
|
||||
.getTypeElement("java.util.function.DoubleSupplier")
|
||||
.asType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoggable(Element element) {
|
||||
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_booleanSupplier)
|
||||
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_intSupplier)
|
||||
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_longSupplier)
|
||||
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_doubleSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String logInvocation(Element element) {
|
||||
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String elementAccess(Element element) {
|
||||
var typeUtils = m_processingEnv.getTypeUtils();
|
||||
var dataType = dataType(element);
|
||||
String base = super.elementAccess(element);
|
||||
|
||||
if (typeUtils.isAssignable(dataType, m_booleanSupplier)) {
|
||||
return base + ".getAsBoolean()";
|
||||
} else if (typeUtils.isAssignable(dataType, m_intSupplier)) {
|
||||
return base + ".getAsInt()";
|
||||
} else if (typeUtils.isAssignable(dataType, m_longSupplier)) {
|
||||
return base + ".getAsLong()";
|
||||
} else if (typeUtils.isAssignable(dataType, m_doubleSupplier)) {
|
||||
return base + ".getAsDouble()";
|
||||
} else {
|
||||
throw new IllegalArgumentException("Element type is unsupported: " + dataType);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import static com.google.testing.compile.CompilationSubject.assertThat;
|
||||
import static com.google.testing.compile.Compiler.javac;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.google.testing.compile.Compilation;
|
||||
import com.google.testing.compile.JavaFileObjects;
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit
|
||||
class EpilogueGeneratorTest {
|
||||
@Test
|
||||
void noFields() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class Example {
|
||||
}
|
||||
""";
|
||||
|
||||
String expected =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.ExampleLogger;
|
||||
|
||||
public final class Epilogue {
|
||||
private static final EpilogueConfiguration config = new EpilogueConfiguration();
|
||||
|
||||
public static final ExampleLogger exampleLogger = new ExampleLogger();
|
||||
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertGeneratedEpilogueContents(source, expected);
|
||||
}
|
||||
|
||||
/** Subclassing RobotBase should not generate the bind() method because it lacks addPeriodic(). */
|
||||
@Test
|
||||
void robotBase() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class Example extends edu.wpi.first.wpilibj.RobotBase {
|
||||
@Override
|
||||
public void startCompetition() {}
|
||||
@Override
|
||||
public void endCompetition() {}
|
||||
}
|
||||
""";
|
||||
|
||||
String expected =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.ExampleLogger;
|
||||
|
||||
public final class Epilogue {
|
||||
private static final EpilogueConfiguration config = new EpilogueConfiguration();
|
||||
|
||||
public static final ExampleLogger exampleLogger = new ExampleLogger();
|
||||
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertGeneratedEpilogueContents(source, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timedRobot() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class Example extends edu.wpi.first.wpilibj.TimedRobot {
|
||||
}
|
||||
""";
|
||||
|
||||
String expected =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.ExampleLogger;
|
||||
|
||||
public final class Epilogue {
|
||||
private static final EpilogueConfiguration config = new EpilogueConfiguration();
|
||||
|
||||
public static final ExampleLogger exampleLogger = new ExampleLogger();
|
||||
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* (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
|
||||
* loop.
|
||||
*/
|
||||
public static void bind(edu.wpi.first.epilogue.Example robot) {
|
||||
robot.addPeriodic(() -> {
|
||||
long start = System.nanoTime();
|
||||
exampleLogger.tryUpdate(config.dataLogger.getSubLogger(config.root), robot, config.errorHandler);
|
||||
edu.wpi.first.networktables.NetworkTableInstance.getDefault().getEntry("Epilogue/Stats/Last Run").setDouble((System.nanoTime() - start) / 1e6);
|
||||
}, robot.getPeriod(), robot.getPeriod() / 2);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertGeneratedEpilogueContents(source, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleRobots() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
@Logged
|
||||
class AlphaBot extends edu.wpi.first.wpilibj.TimedRobot { }
|
||||
|
||||
@Logged
|
||||
class BetaBot extends edu.wpi.first.wpilibj.TimedRobot { }
|
||||
""";
|
||||
|
||||
String expected =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.AlphaBotLogger;
|
||||
import edu.wpi.first.epilogue.BetaBotLogger;
|
||||
|
||||
public final class Epilogue {
|
||||
private static final EpilogueConfiguration config = new EpilogueConfiguration();
|
||||
|
||||
public static final AlphaBotLogger alphaBotLogger = new AlphaBotLogger();
|
||||
public static final BetaBotLogger betaBotLogger = new BetaBotLogger();
|
||||
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* (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
|
||||
* loop.
|
||||
*/
|
||||
public static void bind(edu.wpi.first.epilogue.AlphaBot robot) {
|
||||
robot.addPeriodic(() -> {
|
||||
long start = System.nanoTime();
|
||||
alphaBotLogger.tryUpdate(config.dataLogger.getSubLogger(config.root), robot, config.errorHandler);
|
||||
edu.wpi.first.networktables.NetworkTableInstance.getDefault().getEntry("Epilogue/Stats/Last Run").setDouble((System.nanoTime() - start) / 1e6);
|
||||
}, robot.getPeriod(), robot.getPeriod() / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* (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
|
||||
* loop.
|
||||
*/
|
||||
public static void bind(edu.wpi.first.epilogue.BetaBot robot) {
|
||||
robot.addPeriodic(() -> {
|
||||
long start = System.nanoTime();
|
||||
betaBotLogger.tryUpdate(config.dataLogger.getSubLogger(config.root), robot, config.errorHandler);
|
||||
edu.wpi.first.networktables.NetworkTableInstance.getDefault().getEntry("Epilogue/Stats/Last Run").setDouble((System.nanoTime() - start) / 1e6);
|
||||
}, robot.getPeriod(), robot.getPeriod() / 2);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertGeneratedEpilogueContents(source, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void genericCustomLogger() {
|
||||
String source =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.*;
|
||||
|
||||
class A {}
|
||||
class B extends A {}
|
||||
class C extends A {}
|
||||
|
||||
@CustomLoggerFor({A.class, B.class, C.class})
|
||||
class CustomLogger extends ClassSpecificLogger<A> {
|
||||
public CustomLogger() { super(A.class); }
|
||||
|
||||
@Override
|
||||
public void update(DataLogger logger, A object) {} // implementation is irrelevant
|
||||
}
|
||||
|
||||
@Logged
|
||||
class Example {
|
||||
A a_b_or_c;
|
||||
B b;
|
||||
C c;
|
||||
}
|
||||
""";
|
||||
|
||||
String expected =
|
||||
"""
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.ExampleLogger;
|
||||
import edu.wpi.first.epilogue.CustomLogger;
|
||||
|
||||
public final class Epilogue {
|
||||
private static final EpilogueConfiguration config = new EpilogueConfiguration();
|
||||
|
||||
public static final ExampleLogger exampleLogger = new ExampleLogger();
|
||||
public static final CustomLogger customLogger = new CustomLogger();
|
||||
|
||||
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
|
||||
configurator.accept(config);
|
||||
}
|
||||
|
||||
public static EpilogueConfiguration getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if data associated with a given importance level should be logged.
|
||||
*/
|
||||
public static boolean shouldLog(Logged.Importance importance) {
|
||||
return importance.compareTo(config.minimumImportance) >= 0;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
assertGeneratedEpilogueContents(source, expected);
|
||||
}
|
||||
|
||||
private void assertGeneratedEpilogueContents(
|
||||
String loggedClassContent, String loggerClassContent) {
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withProcessors(new AnnotationProcessor())
|
||||
.compile(JavaFileObjects.forSourceString("", loggedClassContent));
|
||||
|
||||
assertThat(compilation).succeededWithoutWarnings();
|
||||
var generatedFiles = compilation.generatedSourceFiles();
|
||||
var generatedFile =
|
||||
generatedFiles.stream()
|
||||
.filter(jfo -> jfo.getName().contains("Epilogue"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Epilogue file was not generated!"));
|
||||
try {
|
||||
var content = generatedFile.getCharContent(false);
|
||||
assertEquals(
|
||||
loggerClassContent.replace("\r\n", "\n"), content.toString().replace("\r\n", "\n"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StringUtilsTest {
|
||||
@Test
|
||||
void lowerCamelCase() {
|
||||
assertEquals("ioLogger", StringUtils.lowerCamelCase("IOLogger"));
|
||||
assertEquals("ledSubsystem", StringUtils.lowerCamelCase("LEDSubsystem"));
|
||||
assertEquals("fooBar", StringUtils.lowerCamelCase("FooBar"));
|
||||
assertEquals("allcaps", StringUtils.lowerCamelCase("ALLCAPS"));
|
||||
}
|
||||
}
|
||||
16
epilogue-runtime/build.gradle
Normal file
16
epilogue-runtime/build.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
ext {
|
||||
useJava = true
|
||||
useCpp = false
|
||||
baseId = 'epilogue-runtime'
|
||||
groupId = 'edu.wpi.first.epilogue'
|
||||
|
||||
devMain = ''
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/shared/java/javacommon.gradle"
|
||||
|
||||
dependencies {
|
||||
api(project(':ntcore'))
|
||||
api(project(':wpiutil'))
|
||||
api(project(':wpiunits'))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Placed on a subclass of {@code ClassSpecificLogger}. Epilogue will detect it at compile time and
|
||||
* allow logging of data types compatible with the logger.
|
||||
*
|
||||
* <pre><code>
|
||||
* {@literal @}CustomLoggerFor(VendorMotorType.class)
|
||||
* class ExampleMotorLogger extends ClassSpecificLogger<VendorMotorType> { }
|
||||
* </code></pre>
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface CustomLoggerFor {
|
||||
/**
|
||||
* The class or classes of objects able to be logged with the annotated logger.
|
||||
*
|
||||
* @return the supported data types
|
||||
*/
|
||||
Class<?>[] value();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.DataLogger;
|
||||
import edu.wpi.first.epilogue.logging.NTDataLogger;
|
||||
import edu.wpi.first.epilogue.logging.errors.ErrorHandler;
|
||||
import edu.wpi.first.epilogue.logging.errors.ErrorPrinter;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
|
||||
/**
|
||||
* A configuration object to be used by the generated {@code Epilogue} class to customize its
|
||||
* behavior.
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:MemberName")
|
||||
public class EpilogueConfiguration {
|
||||
/**
|
||||
* The data logger implementation for Epilogue to use. By default, this will log data directly to
|
||||
* NetworkTables. NetworkTable data can be mirrored to a log file on disk by calling {@code
|
||||
* DataLogManager.start()} in your {@code robotInit} method.
|
||||
*/
|
||||
public DataLogger dataLogger = new NTDataLogger(NetworkTableInstance.getDefault());
|
||||
|
||||
/**
|
||||
* The minimum importance level of data to be logged. Defaults to debug, which logs data of all
|
||||
* importance levels. Any data tagged with an importance level lower than this will not be logged.
|
||||
*/
|
||||
public Logged.Importance minimumImportance = Logged.Importance.DEBUG;
|
||||
|
||||
/**
|
||||
* The error handler for loggers to use if they encounter an error while logging. Defaults to
|
||||
* printing an error to the standard output.
|
||||
*/
|
||||
public ErrorHandler errorHandler = new ErrorPrinter();
|
||||
|
||||
/**
|
||||
* The root identifier to use for all logged data. Defaults to "Robot", but can be changed to any
|
||||
* string.
|
||||
*/
|
||||
public String root = "Robot";
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Place this annotation on a class to automatically log every field and every public accessor
|
||||
* method (methods with no arguments and return a loggable data type). Use {@link #strategy()} to
|
||||
* flag a class as logging everything it can, except for those elements tagged with
|
||||
* {@code @Logged(importance = NONE)}; or for logging only specific items also tagged with
|
||||
* {@code @Logged}.
|
||||
*
|
||||
* <p>Logged fields may have any access modifier. Logged methods must be public; non-public methods
|
||||
* will be ignored.
|
||||
*
|
||||
* <p>Epilogue can log all primitive types, arrays of primitive types (except char and short),
|
||||
* Strings, arrays of Strings, sendable objects, objects with a struct serializer, and arrays of
|
||||
* objects with struct serializers.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface Logged {
|
||||
/**
|
||||
* The name for the annotated element to be logged as. Does nothing on class-level annotations.
|
||||
* Fields and methods will default to be logged using their in-code names; use this attribute to
|
||||
* set it to something custom.
|
||||
*
|
||||
* <p>If the annotation is placed on a class, the specified name will not change logged data
|
||||
* (since that uses the names of the specific usages of the class in fields and methods); however,
|
||||
* it will be used to set the names of the generated logger that Logged will use to log instances
|
||||
* of the class. This can be used to avoid name conflicts if you have multiple classes with the
|
||||
* same name, but in different packages, and want to be able to log both.
|
||||
*
|
||||
* @return the name to use to log the field or method under; or the name of the generated
|
||||
* class-specific logger
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/** Opt-in or opt-out strategies for logging. */
|
||||
enum Strategy {
|
||||
/**
|
||||
* Log everything except for those elements explicitly opted out of with the skip = true
|
||||
* attribute. This is the default behavior.
|
||||
*/
|
||||
OPT_OUT,
|
||||
|
||||
/** Log only fields and methods tagged with an {@link Logged} annotation. */
|
||||
OPT_IN
|
||||
}
|
||||
|
||||
/**
|
||||
* The strategy to use for logging. Only has an effect on annotations on class or interface
|
||||
* declarations.
|
||||
*
|
||||
* @return the strategy to use to determine which fields and methods in the class to log
|
||||
*/
|
||||
Strategy strategy() default Strategy.OPT_OUT;
|
||||
|
||||
/**
|
||||
* Data importance. Can be used at the class level to set the default importance for all data
|
||||
* points in the class, and can be used on individual fields and methods to set a specific
|
||||
* importance level overriding the class-level default.
|
||||
*/
|
||||
enum Importance {
|
||||
/** Debug information. Useful for low-level information like raw sensor values. */
|
||||
DEBUG,
|
||||
|
||||
/**
|
||||
* Informational data. Useful for higher-level information like pose estimates or subsystem
|
||||
* state.
|
||||
*/
|
||||
INFO,
|
||||
|
||||
/** Critical data that should always be present in logs. */
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
/**
|
||||
* The importance of the annotated data. If placed on a class or interface, this will be the
|
||||
* default importance of all data within that class; this can be overridden on a per-element basis
|
||||
* by annotating fields and methods with their own {@code @Logged(importance = ...)} annotation.
|
||||
*
|
||||
* @return the importance of the annotated element
|
||||
*/
|
||||
Importance importance() default Importance.DEBUG;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* A field or method annotated as {@code @NotLogged} will be ignored by Epilogue when determining
|
||||
* the data to log.
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NotLogged {}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.epilogue.CustomLoggerFor;
|
||||
import edu.wpi.first.epilogue.logging.errors.ErrorHandler;
|
||||
import edu.wpi.first.util.sendable.Sendable;
|
||||
import edu.wpi.first.util.sendable.SendableBuilder;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for class-specific generated loggers. Loggers are generated at compile time by the
|
||||
* Epilogue annotation processor and are used at runtime for zero-overhead data logging. Users may
|
||||
* also declare custom loggers, annotated with {@link CustomLoggerFor @CustomLoggerFor}, for
|
||||
* Epilogue to pull in during compile time to use for logging third party types.
|
||||
*
|
||||
* @param <T> the type of data supported by the logger
|
||||
*/
|
||||
@SuppressWarnings("unused") // Used by generated subclasses
|
||||
public abstract class ClassSpecificLogger<T> {
|
||||
private final Class<T> m_clazz;
|
||||
// TODO: This will hold onto Sendables that are otherwise no longer referenced by a robot program.
|
||||
// Determine if that's a concern
|
||||
// Linked hashmap to maintain insert order
|
||||
private final Map<Sendable, SendableBuilder> m_sendables = new LinkedHashMap<>();
|
||||
|
||||
@SuppressWarnings("PMD.RedundantFieldInitializer")
|
||||
private boolean m_disabled = false;
|
||||
|
||||
/**
|
||||
* Instantiates the logger.
|
||||
*
|
||||
* @param clazz the Java class of objects that can be logged
|
||||
*/
|
||||
protected ClassSpecificLogger(Class<T> clazz) {
|
||||
this.m_clazz = clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an object's fields in a data log.
|
||||
*
|
||||
* @param dataLogger the logger to update
|
||||
* @param object the object to update in the log
|
||||
*/
|
||||
protected abstract void update(DataLogger dataLogger, T object);
|
||||
|
||||
/**
|
||||
* Attempts to update the data log. Will do nothing if the logger is {@link #disable() disabled}.
|
||||
*
|
||||
* @param dataLogger the logger to log data to
|
||||
* @param object the data object to log
|
||||
* @param errorHandler the handler to use if logging raised an exception
|
||||
*/
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
public final void tryUpdate(DataLogger dataLogger, T object, ErrorHandler errorHandler) {
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
update(dataLogger, object);
|
||||
} catch (Exception e) {
|
||||
errorHandler.handle(e, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this logger has been disabled.
|
||||
*
|
||||
* @return true if this logger has been disabled by {@link #disable()}, false if not
|
||||
*/
|
||||
public final boolean isDisabled() {
|
||||
return m_disabled;
|
||||
}
|
||||
|
||||
/** Disables this logger. Any log calls made while disabled will be ignored. */
|
||||
public final void disable() {
|
||||
m_disabled = true;
|
||||
}
|
||||
|
||||
/** Reenables this logger after being disabled. Has no effect if the logger is already enabled. */
|
||||
public final void reenable() {
|
||||
m_disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the data this logger accepts.
|
||||
*
|
||||
* @return the logged data type
|
||||
*/
|
||||
public final Class<T> getLoggedType() {
|
||||
return m_clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a sendable type.
|
||||
*
|
||||
* @param dataLogger the logger to log data into
|
||||
* @param sendable the sendable object to log
|
||||
*/
|
||||
protected void logSendable(DataLogger dataLogger, Sendable sendable) {
|
||||
if (sendable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var builder =
|
||||
m_sendables.computeIfAbsent(
|
||||
sendable,
|
||||
s -> {
|
||||
var b = new LogBackedSendableBuilder(dataLogger);
|
||||
s.initSendable(b);
|
||||
return b;
|
||||
});
|
||||
builder.update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.units.Measure;
|
||||
import edu.wpi.first.units.Unit;
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.Collection;
|
||||
|
||||
/** A data logger is a generic interface for logging discrete data points. */
|
||||
public interface DataLogger {
|
||||
/**
|
||||
* Creates a data logger that logs to multiple backends at once. Data reads will still only occur
|
||||
* once; data is passed to all composed loggers at once.
|
||||
*
|
||||
* @param loggers the loggers to compose together
|
||||
* @return the multi logger
|
||||
*/
|
||||
static DataLogger multi(DataLogger... loggers) {
|
||||
return new MultiLogger(loggers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy version of this logger. A lazy logger will only log data to a field when its
|
||||
* value changes, which can help keep file size and bandwidth usage in check. However, there is an
|
||||
* additional CPU and memory overhead associated with tracking the current value of every logged
|
||||
* entry. The most surefire way to reduce CPU and memory usage associated with logging is to log
|
||||
* fewer things - which can be done by opting out of logging unnecessary data or increasing the
|
||||
* minimum logged importance level in the Epilogue configuration.
|
||||
*
|
||||
* @return the lazy logger
|
||||
*/
|
||||
default DataLogger lazy() {
|
||||
return new LazyLogger(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a logger that can be used to log nested data underneath a specific path.
|
||||
*
|
||||
* @param path the path to use for logging nested data under
|
||||
* @return the sub logger
|
||||
*/
|
||||
DataLogger getSubLogger(String path);
|
||||
|
||||
/**
|
||||
* Logs a 32-bit integer data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, int value);
|
||||
|
||||
/**
|
||||
* Logs a 64-bit integer data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, long value);
|
||||
|
||||
/**
|
||||
* Logs a 32-bit floating point data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, float value);
|
||||
|
||||
/**
|
||||
* Logs a 64-bit floating point data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, double value);
|
||||
|
||||
/**
|
||||
* Logs a boolean data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, boolean value);
|
||||
|
||||
/**
|
||||
* Logs a raw byte array data point. <strong>NOTE:</strong> serializable data should be logged
|
||||
* using {@link #log(String, Object, Struct)}.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, byte[] value);
|
||||
|
||||
/**
|
||||
* Logs a 32-bit integer array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, int[] value);
|
||||
|
||||
/**
|
||||
* Logs a 64-bit integer array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, long[] value);
|
||||
|
||||
/**
|
||||
* Logs a 32-bit floating point array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, float[] value);
|
||||
|
||||
/**
|
||||
* Logs a 64-bit floating point array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, double[] value);
|
||||
|
||||
/**
|
||||
* Logs a boolean array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, boolean[] value);
|
||||
|
||||
/**
|
||||
* Logs a text data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, String value);
|
||||
|
||||
/**
|
||||
* Logs a string array data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
void log(String identifier, String[] value);
|
||||
|
||||
/**
|
||||
* Logs a collection of strings data point.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
*/
|
||||
default void log(String identifier, Collection<String> value) {
|
||||
log(identifier, value.toArray(String[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a struct-serializable object.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
* @param struct the struct to use to serialize the data
|
||||
* @param <S> the serializable type
|
||||
*/
|
||||
<S> void log(String identifier, S value, Struct<S> struct);
|
||||
|
||||
/**
|
||||
* Logs an array of struct-serializable objects.
|
||||
*
|
||||
* @param identifier the identifier of the data point
|
||||
* @param value the value of the data point
|
||||
* @param struct the struct to use to serialize the objects
|
||||
* @param <S> the serializable type
|
||||
*/
|
||||
<S> void log(String identifier, S[] value, Struct<S> struct);
|
||||
|
||||
/**
|
||||
* Logs a collection of struct-serializable objects.
|
||||
*
|
||||
* @param identifier the identifier of the data
|
||||
* @param value the collection of objects to log
|
||||
* @param struct the struct to use to serialize the objects
|
||||
* @param <S> the serializable type
|
||||
*/
|
||||
default <S> void log(String identifier, Collection<S> value, Struct<S> struct) {
|
||||
@SuppressWarnings("unchecked")
|
||||
S[] array = (S[]) value.toArray(Object[]::new);
|
||||
log(identifier, array, struct);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a measurement's value in terms of its base unit.
|
||||
*
|
||||
* @param identifier the identifier of the data field
|
||||
* @param value the new value of the data field
|
||||
*/
|
||||
default void log(String identifier, Measure<?> value) {
|
||||
log(identifier, value.baseUnitMagnitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a measurement's value in terms of another unit.
|
||||
*
|
||||
* @param identifier the identifier of the data field
|
||||
* @param value the new value of the data field
|
||||
* @param unit the unit to log the measurement in
|
||||
* @param <U> the dimension of the unit
|
||||
*/
|
||||
default <U extends Unit<U>> void log(String identifier, Measure<U> value, U unit) {
|
||||
log(identifier, value.in(unit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an enum value. The value will appear as a string entry using the name of the enum.
|
||||
*
|
||||
* @param identifier the identifier of the data field
|
||||
* @param value the new value of the data field
|
||||
*/
|
||||
default void log(String identifier, Enum<?> value) {
|
||||
log(identifier, value.name());
|
||||
}
|
||||
|
||||
// TODO: Add default methods to support common no-struct no-sendable types like joysticks?
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
|
||||
|
||||
import edu.wpi.first.util.datalog.BooleanArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.BooleanLogEntry;
|
||||
import edu.wpi.first.util.datalog.DataLog;
|
||||
import edu.wpi.first.util.datalog.DataLogEntry;
|
||||
import edu.wpi.first.util.datalog.DoubleArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.DoubleLogEntry;
|
||||
import edu.wpi.first.util.datalog.FloatArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.FloatLogEntry;
|
||||
import edu.wpi.first.util.datalog.IntegerArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.IntegerLogEntry;
|
||||
import edu.wpi.first.util.datalog.RawLogEntry;
|
||||
import edu.wpi.first.util.datalog.StringArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.StringLogEntry;
|
||||
import edu.wpi.first.util.datalog.StructArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.StructLogEntry;
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/** A data logger implementation that saves information to a WPILib {@link DataLog} file on disk. */
|
||||
public class FileLogger implements DataLogger {
|
||||
private final DataLog m_dataLog;
|
||||
private final Map<String, DataLogEntry> m_entries = new HashMap<>();
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new file logger.
|
||||
*
|
||||
* @param dataLog the data log to save data to
|
||||
*/
|
||||
public FileLogger(DataLog dataLog) {
|
||||
this.m_dataLog = requireNonNullParam(dataLog, "dataLog", "FileLogger");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <E extends DataLogEntry> E getEntry(
|
||||
String identifier, BiFunction<DataLog, String, ? extends E> ctor) {
|
||||
if (m_entries.get(identifier) != null) {
|
||||
return (E) m_entries.get(identifier);
|
||||
}
|
||||
|
||||
var entry = ctor.apply(m_dataLog, identifier);
|
||||
m_entries.put(identifier, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
getEntry(identifier, IntegerLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
getEntry(identifier, IntegerLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
getEntry(identifier, FloatLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
getEntry(identifier, DoubleLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
getEntry(identifier, BooleanLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
getEntry(identifier, RawLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.UnnecessaryCastRule")
|
||||
public void log(String identifier, int[] value) {
|
||||
long[] widened = new long[value.length];
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
widened[i] = (long) value[i];
|
||||
}
|
||||
getEntry(identifier, IntegerArrayLogEntry::new).append(widened);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
getEntry(identifier, IntegerArrayLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
getEntry(identifier, FloatArrayLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
getEntry(identifier, DoubleArrayLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
getEntry(identifier, BooleanArrayLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
getEntry(identifier, StringLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
getEntry(identifier, StringArrayLogEntry::new).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_dataLog.addSchema(struct);
|
||||
getEntry(identifier, (log, k) -> StructLogEntry.create(log, k, struct)).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_dataLog.addSchema(struct);
|
||||
getEntry(identifier, (log, k) -> StructArrayLogEntry.create(log, k, struct)).append(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A data logger implementation that only logs data when it changes. Useful for keeping bandwidth
|
||||
* and file sizes down. However, because it still needs to check that data has changed, it cannot
|
||||
* avoid expensive sensor reads.
|
||||
*/
|
||||
public class LazyLogger implements DataLogger {
|
||||
private final DataLogger m_logger;
|
||||
|
||||
// Keep a record of the most recent value written to each entry
|
||||
// Note that this may duplicate a lot of data, and will box primitives.
|
||||
private final Map<String, Object> m_previousValues = new HashMap<>();
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new lazy logger wrapper around another logger.
|
||||
*
|
||||
* @param logger the logger to delegate to
|
||||
*/
|
||||
public LazyLogger(DataLogger logger) {
|
||||
this.m_logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger lazy() {
|
||||
// Already lazy, don't need to wrap it again
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Integer oldValue && oldValue == value) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Long oldValue && oldValue == value) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Float oldValue && oldValue == value) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Double oldValue && oldValue == value) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Boolean oldValue && oldValue == value) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof byte[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof int[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof long[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof float[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof double[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof boolean[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof String oldValue && oldValue.equals(value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof String[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (Objects.equals(previous, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value, struct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
var previous = m_previousValues.get(identifier);
|
||||
|
||||
if (previous instanceof Object[] oldValue && Arrays.equals(oldValue, value)) {
|
||||
// no change
|
||||
return;
|
||||
}
|
||||
|
||||
m_previousValues.put(identifier, value);
|
||||
m_logger.log(identifier, value, struct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.function.BooleanConsumer;
|
||||
import edu.wpi.first.util.function.FloatConsumer;
|
||||
import edu.wpi.first.util.function.FloatSupplier;
|
||||
import edu.wpi.first.util.sendable.SendableBuilder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.DoubleConsumer;
|
||||
import java.util.function.DoubleSupplier;
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** A sendable builder implementation that sends data to a {@link DataLogger}. */
|
||||
@SuppressWarnings("PMD.CouplingBetweenObjects") // most methods simply delegate to the logger
|
||||
public class LogBackedSendableBuilder implements SendableBuilder {
|
||||
private final DataLogger m_logger;
|
||||
private final Collection<Runnable> m_updates = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new sendable builder that delegates writes to an underlying data logger.
|
||||
*
|
||||
* @param logger the data logger to write the sendable data to
|
||||
*/
|
||||
public LogBackedSendableBuilder(DataLogger logger) {
|
||||
this.m_logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSmartDashboardType(String type) {
|
||||
m_logger.log(".type", type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActuator(boolean value) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSafeState(Runnable func) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.getAsBoolean()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstBoolean(String key, boolean value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIntegerProperty(String key, LongSupplier getter, LongConsumer setter) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.getAsLong()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstInteger(String key, long value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFloatProperty(String key, FloatSupplier getter, FloatConsumer setter) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.getAsFloat()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstFloat(String key, float value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDoubleProperty(String key, DoubleSupplier getter, DoubleConsumer setter) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.getAsDouble()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstDouble(String key, double value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStringProperty(String key, Supplier<String> getter, Consumer<String> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstString(String key, String value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBooleanArrayProperty(
|
||||
String key, Supplier<boolean[]> getter, Consumer<boolean[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstBooleanArray(String key, boolean[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIntegerArrayProperty(
|
||||
String key, Supplier<long[]> getter, Consumer<long[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstIntegerArray(String key, long[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFloatArrayProperty(
|
||||
String key, Supplier<float[]> getter, Consumer<float[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstFloatArray(String key, float[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDoubleArrayProperty(
|
||||
String key, Supplier<double[]> getter, Consumer<double[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstDoubleArray(String key, double[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStringArrayProperty(
|
||||
String key, Supplier<String[]> getter, Consumer<String[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstStringArray(String key, String[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRawProperty(
|
||||
String key, String typeString, Supplier<byte[]> getter, Consumer<byte[]> setter) {
|
||||
if (getter != null) {
|
||||
m_updates.add(() -> m_logger.log(key, getter.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishConstRaw(String key, String typeString, byte[] value) {
|
||||
m_logger.log(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BackendKind getBackendKind() {
|
||||
return BackendKind.kUnknown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPublished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
for (Runnable update : m_updates) {
|
||||
update.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearProperties() {
|
||||
m_updates.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCloseable(AutoCloseable closeable) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
clearProperties();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data logger implementation that delegates to other loggers. Helpful for simultaneous logging to
|
||||
* multiple data stores at once.
|
||||
*/
|
||||
public class MultiLogger implements DataLogger {
|
||||
private final List<DataLogger> m_loggers;
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
// Use DataLogger.multi(...) instead of instantiation directly
|
||||
MultiLogger(DataLogger... loggers) {
|
||||
this.m_loggers = List.of(loggers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value, struct);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
for (DataLogger logger : m_loggers) {
|
||||
logger.log(identifier, value, struct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.networktables.BooleanArrayPublisher;
|
||||
import edu.wpi.first.networktables.BooleanPublisher;
|
||||
import edu.wpi.first.networktables.DoubleArrayPublisher;
|
||||
import edu.wpi.first.networktables.DoublePublisher;
|
||||
import edu.wpi.first.networktables.FloatArrayPublisher;
|
||||
import edu.wpi.first.networktables.FloatPublisher;
|
||||
import edu.wpi.first.networktables.IntegerArrayPublisher;
|
||||
import edu.wpi.first.networktables.IntegerPublisher;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.Publisher;
|
||||
import edu.wpi.first.networktables.RawPublisher;
|
||||
import edu.wpi.first.networktables.StringArrayPublisher;
|
||||
import edu.wpi.first.networktables.StringPublisher;
|
||||
import edu.wpi.first.networktables.StructArrayPublisher;
|
||||
import edu.wpi.first.networktables.StructPublisher;
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data logger implementation that sends data over network tables. Be careful when using this,
|
||||
* since sending too much data may cause bandwidth or CPU starvation.
|
||||
*/
|
||||
public class NTDataLogger implements DataLogger {
|
||||
private final NetworkTableInstance m_nt;
|
||||
|
||||
private final Map<String, Publisher> m_publishers = new HashMap<>();
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a data logger that sends information to NetworkTables.
|
||||
*
|
||||
* @param nt the NetworkTable instance to use to send data to
|
||||
*/
|
||||
public NTDataLogger(NetworkTableInstance nt) {
|
||||
this.m_nt = nt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
((IntegerPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
((IntegerPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
((FloatPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
((DoublePublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
((BooleanPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
((RawPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getRawTopic(k).publish("raw")))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PMD.UnnecessaryCastRule")
|
||||
public void log(String identifier, int[] value) {
|
||||
// NT backend only supports int64[], so we have to manually widen to 64 bits before sending
|
||||
long[] widened = new long[value.length];
|
||||
|
||||
for (int i = 0; i < value.length; i++) {
|
||||
widened[i] = (long) value[i];
|
||||
}
|
||||
|
||||
((IntegerArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
|
||||
.set(widened);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
((IntegerArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
((FloatArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatArrayTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
((DoubleArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleArrayTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
((BooleanArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanArrayTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
((StringPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
((StringArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringArrayTopic(k).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_nt.addSchema(struct);
|
||||
((StructPublisher<S>)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStructTopic(k, struct).publish()))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_nt.addSchema(struct);
|
||||
((StructArrayPublisher<S>)
|
||||
m_publishers.computeIfAbsent(
|
||||
identifier, k -> m_nt.getStructArrayTopic(k, struct).publish()))
|
||||
.set(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
|
||||
/** Null data logger implementation that logs nothing. */
|
||||
public class NullLogger implements DataLogger {
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
// Since a sublogger would still log nothing and has no state, we can just return the same
|
||||
// null-logging implementation
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data logger that logs to an underlying logger, prepending all logged data with a specific
|
||||
* prefix. Useful for logging nested data structures.
|
||||
*/
|
||||
public class SubLogger implements DataLogger {
|
||||
private final String m_prefix;
|
||||
private final DataLogger m_impl;
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new sublogger underneath another logger.
|
||||
*
|
||||
* @param prefix the prefix to append to all data logged in the sublogger
|
||||
* @param impl the data logger to log to
|
||||
*/
|
||||
public SubLogger(String prefix, DataLogger impl) {
|
||||
// Add a trailing slash if not already present
|
||||
if (prefix.endsWith("/")) {
|
||||
this.m_prefix = prefix;
|
||||
} else {
|
||||
this.m_prefix = prefix + "/";
|
||||
}
|
||||
this.m_impl = impl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_impl.log(m_prefix + identifier, value, struct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_impl.log(m_prefix + identifier, value, struct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging.errors;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
|
||||
/**
|
||||
* An error handler implementation that will throw an exception if logging raised an exception. This
|
||||
* is useful when running code in simulation or in JUnit tests to quickly identify errors in your
|
||||
* code.
|
||||
*/
|
||||
public class CrashOnError implements ErrorHandler {
|
||||
@Override
|
||||
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
|
||||
throw new RuntimeException(
|
||||
"[EPILOGUE] An error occurred while logging an instance of "
|
||||
+ logger.getLoggedType().getName(),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging.errors;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
|
||||
/**
|
||||
* An error handler is used by the Logged framework to catch and process any errors that occur
|
||||
* during the logging process. Different handlers can be used in different operating modes, such as
|
||||
* crashing in simulation to identify errors before they make it to a robot, or automatically
|
||||
* disabling loggers if they encounter too many errors on the field to let the robot keep running
|
||||
* while playing a match.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ErrorHandler {
|
||||
/**
|
||||
* Handles an exception that arose while logging.
|
||||
*
|
||||
* @param exception the exception that occurred
|
||||
* @param logger the logger that was being processed that caused the error to occur
|
||||
*/
|
||||
void handle(Throwable exception, ClassSpecificLogger<?> logger);
|
||||
|
||||
/**
|
||||
* Creates an error handler that will immediately re-throw an exception and cause robot code to
|
||||
* exit. This is particularly useful when running in simulation or JUnit tests to identify errors
|
||||
* quickly and safely.
|
||||
*
|
||||
* @return the error handler
|
||||
*/
|
||||
static ErrorHandler crashOnError() {
|
||||
return new CrashOnError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error handler that will print error messages to the console output, but otherwise
|
||||
* allow logging to continue in the future. This can be helpful when errors occur only rarely and
|
||||
* you don't want your robot program to crash or disable future logging.
|
||||
*
|
||||
* @return the error handler
|
||||
*/
|
||||
static ErrorHandler printErrorMessages() {
|
||||
return new ErrorPrinter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error handler that will automatically disable a logger if it encounters too many
|
||||
* errors. Only the error-prone logger(s) will be disabled; loggers that have not encountered any
|
||||
* errors, or encountered fewer than the limit, will continue to be used. Disabled loggers can be
|
||||
* reset by calling {@link LoggerDisabler#reset()} on the handler.
|
||||
*
|
||||
* @param maximumPermissibleErrors the maximum number of errors that a logger is permitted to
|
||||
* encounter before being disabled.
|
||||
* @return the error handler
|
||||
*/
|
||||
static LoggerDisabler disabling(int maximumPermissibleErrors) {
|
||||
return LoggerDisabler.forLimit(maximumPermissibleErrors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging.errors;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
|
||||
/** An error handler implementation that prints error information to the console. */
|
||||
public class ErrorPrinter implements ErrorHandler {
|
||||
@Override
|
||||
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
|
||||
System.err.println(
|
||||
"[EPILOGUE] An error occurred while logging an instance of "
|
||||
+ logger.getLoggedType().getName()
|
||||
+ ": "
|
||||
+ exception.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging.errors;
|
||||
|
||||
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An error handler that disables loggers after too many exceptions are raised. Useful when playing
|
||||
* in matches, where data logging is less important than reliable control. Setting the threshold to
|
||||
* ≤0 will cause any logger that encounters an exception whilst logging to immediately be disabled.
|
||||
* Setting to higher values means your program is more tolerant of errors, but takes longer to
|
||||
* disable, and therefore may have more sets of partial or incomplete data and may have more CPU
|
||||
* overhead due to the cost of throwing exceptions.
|
||||
*/
|
||||
public class LoggerDisabler implements ErrorHandler {
|
||||
private final int m_threshold;
|
||||
private final Map<ClassSpecificLogger<?>, Integer> m_errorCounts = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new logger-disabling error handler.
|
||||
*
|
||||
* @param threshold how many errors any one logger is allowed to encounter before it is disabled.
|
||||
*/
|
||||
public LoggerDisabler(int threshold) {
|
||||
this.m_threshold = threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disabler that kicks in after a logger raises more than {@code threshold} exceptions.
|
||||
*
|
||||
* @param threshold the threshold value for the maximum number of exceptions loggers are permitted
|
||||
* to encounter before they are disabled
|
||||
* @return the disabler
|
||||
*/
|
||||
public static LoggerDisabler forLimit(int threshold) {
|
||||
return new LoggerDisabler(threshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
|
||||
var errorCount = m_errorCounts.getOrDefault(logger, 0) + 1;
|
||||
m_errorCounts.put(logger, errorCount);
|
||||
|
||||
if (errorCount > m_threshold) {
|
||||
logger.disable();
|
||||
System.err.println(
|
||||
"[EPILOGUE] Too many errors detected in "
|
||||
+ logger.getClass().getName()
|
||||
+ " (maximum allowed: "
|
||||
+ m_threshold
|
||||
+ "). The most recent error follows:");
|
||||
System.err.println(exception.getMessage());
|
||||
exception.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Resets all error counts and reenables all loggers. */
|
||||
public void reset() {
|
||||
for (var logger : m_errorCounts.keySet()) {
|
||||
// Safe. This is a no-op on loggers that are already enabled
|
||||
logger.reenable();
|
||||
}
|
||||
m_errorCounts.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ClassSpecificLoggerTest {
|
||||
@Logged
|
||||
record Point2d(double x, double y, int dim) {
|
||||
static class Logger extends ClassSpecificLogger<Point2d> {
|
||||
Logger() {
|
||||
super(Point2d.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void update(DataLogger dataLogger, Point2d object) {
|
||||
dataLogger.log("x", object.x);
|
||||
dataLogger.log("y", object.y);
|
||||
dataLogger.log("dim", object.dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReadPrivate() {
|
||||
var point = new Point2d(1, 4, 2);
|
||||
var logger = new Point2d.Logger();
|
||||
var dataLog = new TestLogger();
|
||||
logger.update(dataLog.getSubLogger("Point"), point);
|
||||
|
||||
assertEquals(
|
||||
List.of(
|
||||
new TestLogger.LogEntry<>("Point/x", 1.0),
|
||||
new TestLogger.LogEntry<>("Point/y", 4.0),
|
||||
new TestLogger.LogEntry<>("Point/dim", 2)),
|
||||
dataLog.getEntries());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LazyLoggerTest {
|
||||
@Test
|
||||
void lazyOfLazyReturnsSelf() {
|
||||
var lazy = new LazyLogger(new NullLogger());
|
||||
assertSame(lazy, lazy.lazy());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyInt() {
|
||||
var logger = new TestLogger();
|
||||
var lazy = new LazyLogger(logger);
|
||||
|
||||
{
|
||||
// First time logging to "int" should go through
|
||||
lazy.log("int", 0);
|
||||
assertEquals(List.of(new TestLogger.LogEntry<>("int", 0)), logger.getEntries());
|
||||
}
|
||||
|
||||
{
|
||||
// Logging the current value shouldn't go through
|
||||
lazy.log("int", 0);
|
||||
assertEquals(List.of(new TestLogger.LogEntry<>("int", 0)), logger.getEntries());
|
||||
}
|
||||
|
||||
{
|
||||
// Logging a new value should go through
|
||||
lazy.log("int", 1);
|
||||
assertEquals(
|
||||
List.of(new TestLogger.LogEntry<>("int", 0), new TestLogger.LogEntry<>("int", 1)),
|
||||
logger.getEntries());
|
||||
}
|
||||
|
||||
{
|
||||
// Logging a previous value should go through
|
||||
lazy.log("int", 0);
|
||||
assertEquals(
|
||||
List.of(
|
||||
new TestLogger.LogEntry<>("int", 0),
|
||||
new TestLogger.LogEntry<>("int", 1),
|
||||
new TestLogger.LogEntry<>("int", 0)),
|
||||
logger.getEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) FIRST and other WPILib contributors.
|
||||
// Open Source Software; you can modify and/or share it under the terms of
|
||||
// the WPILib BSD license file in the root directory of this project.
|
||||
|
||||
package edu.wpi.first.epilogue.logging;
|
||||
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import edu.wpi.first.util.struct.StructBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("PMD.TestClassWithoutTestCases") // This is not a test class!
|
||||
public class TestLogger implements DataLogger {
|
||||
public record LogEntry<T>(String identifier, T value) {}
|
||||
|
||||
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
|
||||
|
||||
private final List<LogEntry<?>> m_entries = new ArrayList<>();
|
||||
|
||||
public List<LogEntry<?>> getEntries() {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataLogger getSubLogger(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
m_entries.add(new LogEntry<>(identifier, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
var serialized = StructBuffer.create(struct).write(value).array();
|
||||
|
||||
m_entries.add(new LogEntry<>(identifier, serialized));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
var serialized = StructBuffer.create(struct).writeArray(value).array();
|
||||
|
||||
m_entries.add(new LogEntry<>(identifier, serialized));
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@ include 'msvcruntime'
|
||||
include 'ntcoreffi'
|
||||
include 'apriltag'
|
||||
include 'processstarter'
|
||||
include 'epilogue-processor'
|
||||
include 'epilogue-runtime'
|
||||
|
||||
buildCache {
|
||||
def cred = {
|
||||
|
||||
@@ -113,6 +113,8 @@ tasks.withType(JavaCompile).configureEach {
|
||||
"-Xlint:-try",
|
||||
// ignore missing serialVersionUID warnings
|
||||
"-Xlint:-serial",
|
||||
// ignore unclaimed annotation warning from annotation processing
|
||||
"-Xlint:-processing",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ dependencies {
|
||||
implementation project(':wpilibNewCommands')
|
||||
implementation project(':romiVendordep')
|
||||
implementation project(':xrpVendordep')
|
||||
implementation project(':epilogue-runtime')
|
||||
annotationProcessor project(':epilogue-processor')
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
|
||||
@@ -6,6 +6,7 @@ package edu.wpi.first.wpilibj.examples.rapidreactcommandbot;
|
||||
|
||||
import static edu.wpi.first.wpilibj2.command.Commands.parallel;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.AutoConstants;
|
||||
import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.OIConstants;
|
||||
import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.ShooterConstants;
|
||||
@@ -24,6 +25,7 @@ import edu.wpi.first.wpilibj2.command.button.Trigger;
|
||||
* periodic methods (other than the scheduler calls). Instead, the structure of the robot (including
|
||||
* subsystems, commands, and button mappings) should be declared here.
|
||||
*/
|
||||
@Logged(name = "Rapid React Command Robot Container")
|
||||
public class RapidReactCommandBot {
|
||||
// The robot's subsystems
|
||||
private final Drive m_drive = new Drive();
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.rapidreactcommandbot;
|
||||
|
||||
import edu.wpi.first.epilogue.Epilogue;
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.wpilibj.DataLogManager;
|
||||
import edu.wpi.first.wpilibj.TimedRobot;
|
||||
import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.CommandScheduler;
|
||||
@@ -14,6 +17,7 @@ import edu.wpi.first.wpilibj2.command.CommandScheduler;
|
||||
* the package after creating this project, you must also update the build.gradle file in the
|
||||
* project.
|
||||
*/
|
||||
@Logged(name = "Rapid React Command Robot")
|
||||
public class Robot extends TimedRobot {
|
||||
private Command m_autonomousCommand;
|
||||
|
||||
@@ -27,6 +31,10 @@ public class Robot extends TimedRobot {
|
||||
public void robotInit() {
|
||||
// Configure default commands and condition bindings on robot startup
|
||||
m_robot.configureBindings();
|
||||
|
||||
// Initialize data logging.
|
||||
DataLogManager.start();
|
||||
Epilogue.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.rapidreactcommandbot.subsystems;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.NotLogged;
|
||||
import edu.wpi.first.util.sendable.SendableRegistry;
|
||||
import edu.wpi.first.wpilibj.Encoder;
|
||||
import edu.wpi.first.wpilibj.drive.DifferentialDrive;
|
||||
@@ -13,6 +15,7 @@ import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
import java.util.function.DoubleSupplier;
|
||||
|
||||
@Logged
|
||||
public class Drive extends SubsystemBase {
|
||||
// The motors on the left side of the drive.
|
||||
private final PWMSparkMax m_leftLeader = new PWMSparkMax(DriveConstants.kLeftMotor1Port);
|
||||
@@ -23,6 +26,7 @@ public class Drive extends SubsystemBase {
|
||||
private final PWMSparkMax m_rightFollower = new PWMSparkMax(DriveConstants.kRightMotor2Port);
|
||||
|
||||
// The robot's drive
|
||||
@NotLogged // Would duplicate motor data, there's no point sending it twice
|
||||
private final DifferentialDrive m_drive =
|
||||
new DifferentialDrive(m_leftLeader::set, m_rightLeader::set);
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ package edu.wpi.first.wpilibj.examples.rapidreactcommandbot.subsystems;
|
||||
|
||||
import static edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.IntakeConstants;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.wpilibj.DoubleSolenoid;
|
||||
import edu.wpi.first.wpilibj.PneumaticsModuleType;
|
||||
import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
|
||||
import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
|
||||
@Logged
|
||||
public class Intake extends SubsystemBase {
|
||||
private final PWMSparkMax m_motor = new PWMSparkMax(IntakeConstants.kMotorPort);
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.rapidreactcommandbot.subsystems;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.wpilibj.AnalogPotentiometer;
|
||||
import edu.wpi.first.wpilibj.Compressor;
|
||||
import edu.wpi.first.wpilibj.PneumaticsModuleType;
|
||||
import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard;
|
||||
import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
|
||||
/** Subsystem for managing the compressor, pressure sensor, etc. */
|
||||
@Logged
|
||||
public class Pneumatics extends SubsystemBase {
|
||||
// External analog pressure sensor
|
||||
// product-specific voltage->pressure conversion, see product manual
|
||||
@@ -26,17 +27,12 @@ public class Pneumatics extends SubsystemBase {
|
||||
// Compressor connected to a PCM with a default CAN ID (0)
|
||||
private final Compressor m_compressor = new Compressor(PneumaticsModuleType.CTREPCM);
|
||||
|
||||
public Pneumatics() {
|
||||
var tab = Shuffleboard.getTab("Pneumatics");
|
||||
tab.addDouble("External Pressure [PSI]", this::getPressure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the analog pressure sensor.
|
||||
*
|
||||
* @return the measured pressure, in PSI
|
||||
*/
|
||||
private double getPressure() {
|
||||
public double getPressure() {
|
||||
// Get the pressure (in PSI) from an analog pressure sensor connected to the RIO.
|
||||
return m_pressureTransducer.get();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import static edu.wpi.first.units.Units.Volts;
|
||||
import static edu.wpi.first.wpilibj2.command.Commands.parallel;
|
||||
import static edu.wpi.first.wpilibj2.command.Commands.waitUntil;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.math.controller.PIDController;
|
||||
import edu.wpi.first.math.controller.SimpleMotorFeedforward;
|
||||
import edu.wpi.first.wpilibj.Encoder;
|
||||
@@ -17,6 +18,7 @@ import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
|
||||
import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
|
||||
@Logged
|
||||
public class Shooter extends SubsystemBase {
|
||||
private final PWMSparkMax m_shooterMotor = new PWMSparkMax(ShooterConstants.kShooterMotorPort);
|
||||
private final PWMSparkMax m_feederMotor = new PWMSparkMax(ShooterConstants.kFeederMotorPort);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package edu.wpi.first.wpilibj.examples.rapidreactcommandbot.subsystems;
|
||||
|
||||
import edu.wpi.first.epilogue.Logged;
|
||||
import edu.wpi.first.epilogue.NotLogged;
|
||||
import edu.wpi.first.wpilibj.DigitalInput;
|
||||
import edu.wpi.first.wpilibj.examples.rapidreactcommandbot.Constants.StorageConstants;
|
||||
import edu.wpi.first.wpilibj.motorcontrol.PWMSparkMax;
|
||||
@@ -11,13 +13,16 @@ import edu.wpi.first.wpilibj2.command.Command;
|
||||
import edu.wpi.first.wpilibj2.command.SubsystemBase;
|
||||
import edu.wpi.first.wpilibj2.command.button.Trigger;
|
||||
|
||||
@Logged
|
||||
public class Storage extends SubsystemBase {
|
||||
private final PWMSparkMax m_motor = new PWMSparkMax(StorageConstants.kMotorPort);
|
||||
@NotLogged // We'll log a more meaningful boolean instead
|
||||
private final DigitalInput m_ballSensor = new DigitalInput(StorageConstants.kBallSensorPort);
|
||||
|
||||
// Expose trigger from subsystem to improve readability and ease
|
||||
// inter-subsystem communications
|
||||
/** Whether the ball storage is full. */
|
||||
@Logged(name = "Has Cargo")
|
||||
@SuppressWarnings("checkstyle:MemberName")
|
||||
public final Trigger hasCargo = new Trigger(m_ballSensor::get);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user