diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java index 398569702e..c4caac6a0e 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java @@ -112,7 +112,8 @@ public class AnnotationProcessor extends AbstractProcessor { new MeasureHandler(processingEnv), new PrimitiveHandler(processingEnv), new SupplierHandler(processingEnv), - new StructHandler(processingEnv), // prioritize struct over sendable + new StructHandler(processingEnv), // prioritize struct over sendable and protobuf + new ProtobufHandler(processingEnv), // then protobuf new SendableHandler(processingEnv)); m_epiloguerGenerator = new EpilogueGenerator(processingEnv, customLoggers); diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ProtobufHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ProtobufHandler.java new file mode 100644 index 0000000000..63186b1640 --- /dev/null +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ProtobufHandler.java @@ -0,0 +1,102 @@ +// 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.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Supports protobuf serializable types. Protobuf-serializable types are loggable if they have a + * public static final {@code proto} field of a type that inherits from {@code Protobuf}. + */ +public class ProtobufHandler extends ElementHandler { + private final TypeMirror m_serializable; + private final TypeElement m_protobufType; + private final Types m_typeUtils; + private final Elements m_elementUtils; + + protected ProtobufHandler(ProcessingEnvironment processingEnv) { + super(processingEnv); + + m_serializable = + processingEnv + .getElementUtils() + .getTypeElement("edu.wpi.first.util.protobuf.ProtobufSerializable") + .asType(); + m_protobufType = + processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.protobuf.Protobuf"); + m_typeUtils = processingEnv.getTypeUtils(); + m_elementUtils = processingEnv.getElementUtils(); + } + + @Override + public boolean isLoggable(Element element) { + return isLoggableType(dataType(element)); + } + + /** + * Checks if a type is protobuf-serializable: implements the ProtobufSerializable marker interface + * and has a `public static final proto` field of a type that inherits from Protobuf with a + * compatible generic type bound. + * + * @param type The type to check + * @return true if the type is protobuf-serializable, false otherwise + */ + public boolean isLoggableType(TypeMirror type) { + var serializableType = m_typeUtils.erasure(type); + var typeElement = m_elementUtils.getTypeElement(serializableType.toString()); + if (typeElement == null) { + return false; + } + + // eg `Protobuf` instead of the raw `Protobuf` type. The message type doesn't + // really matter here; we can leave it as a wildcard. + var sharpProtobufType = + m_typeUtils.getDeclaredType( + m_protobufType, + typeElement.asType(), // the serializable type + m_typeUtils.getWildcardType( + m_elementUtils.getTypeElement("us.hebi.quickbuf.ProtoMessage").asType(), null)); + + boolean hasProto = + typeElement.getEnclosedElements().stream() + .filter(e -> e instanceof VariableElement) + .map(e -> (VariableElement) e) + .anyMatch( + field -> { + var nameMatch = field.getSimpleName().contentEquals("proto"); + var modifiersMatch = + field + .getModifiers() + .containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)); + var typeMatch = + m_typeUtils.isAssignable( + m_typeUtils.erasure(field.asType()), sharpProtobufType); + return nameMatch && modifiersMatch && typeMatch; + }); + return m_typeUtils.isAssignable(type, m_serializable) && hasProto; + } + + public String protoAccess(TypeMirror serializableType) { + var className = m_typeUtils.erasure(serializableType).toString(); + return className + ".proto"; + } + + @Override + public String logInvocation(Element element, TypeElement loggedClass) { + return "backend.log(\"%s\", %s, %s)" + .formatted( + loggedName(element), + elementAccess(element, loggedClass), + protoAccess(dataType(element))); + } +} diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StructHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StructHandler.java index 1c658818e6..a6063227d5 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StructHandler.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StructHandler.java @@ -4,15 +4,26 @@ package edu.wpi.first.epilogue.processor; +import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +/** + * Supports struct serializable types. Struct-serializable types are loggable if they have a public + * static final {@code struct} field of a type that inherits from {@code Struct}. + */ public class StructHandler extends ElementHandler { private final TypeMirror m_serializable; + private final TypeElement m_structType; private final Types m_typeUtils; + private final Elements m_elementUtils; protected StructHandler(ProcessingEnvironment processingEnv) { super(processingEnv); @@ -21,16 +32,57 @@ public class StructHandler extends ElementHandler { .getElementUtils() .getTypeElement("edu.wpi.first.util.struct.StructSerializable") .asType(); + m_structType = + processingEnv.getElementUtils().getTypeElement("edu.wpi.first.util.struct.Struct"); m_typeUtils = processingEnv.getTypeUtils(); + m_elementUtils = processingEnv.getElementUtils(); } @Override public boolean isLoggable(Element element) { - return m_typeUtils.isAssignable(dataType(element), m_serializable); + return isLoggableType(dataType(element)); } + /** + * Checks if a type is struct-serializable: implements the StructSerializable marker interface and + * has a `public static final struct` field of a type that inherits from Struct with a compatible + * generic type bound. + * + * @param type The type to check + * @return true if the type is struct-serializable, false otherwise + */ public boolean isLoggableType(TypeMirror type) { - return m_typeUtils.isAssignable(type, m_serializable); + TypeMirror serializableType; + if (type instanceof ArrayType arr) { + serializableType = arr.getComponentType(); + } else { + serializableType = m_typeUtils.erasure(type); + } + var typeElement = m_elementUtils.getTypeElement(serializableType.toString()); + if (typeElement == null) { + return false; + } + + // eg `Struct` instead of the raw `Struct` type + var sharpStructType = m_typeUtils.getDeclaredType(m_structType, typeElement.asType()); + + boolean hasStruct = + typeElement.getEnclosedElements().stream() + .filter(e -> e instanceof VariableElement) + .map(e -> (VariableElement) e) + .anyMatch( + field -> { + var nameMatch = field.getSimpleName().contentEquals("struct"); + var modifiersMatch = + field + .getModifiers() + .containsAll(Set.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)); + var typeMatch = + m_typeUtils.isAssignable( + m_typeUtils.erasure(field.asType()), sharpStructType); + return nameMatch && modifiersMatch && typeMatch; + }); + return m_typeUtils.isAssignable(type, m_serializable) && hasStruct; } public String structAccess(TypeMirror serializableType) { diff --git a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java index 65a7a2e736..6b30239fab 100644 --- a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java +++ b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java @@ -1141,6 +1141,76 @@ class AnnotationProcessorTest { assertLoggerGenerates(source, expectedGeneratedSource); } + @Test + void protobuf() { + String source = + """ + package edu.wpi.first.epilogue; + + import edu.wpi.first.util.protobuf.Protobuf; + import edu.wpi.first.util.protobuf.ProtobufSerializable; + import java.util.List; + import us.hebi.quickbuf.*; + + class ProtobufType implements ProtobufSerializable { + // Message type is necessary - Epilogue can't log with a wildcard message type in the proto + public static final Protobuf proto = null; // value doesn't matter + + static class Message extends ProtoMessage { + // Implement stubs for the abstract base class. + // This code never runs so actual implementations are unnecessary. + @Override + public Message copyFrom(Message other) { return null; } + @Override + public Message clear() { return null; } + @Override + public int computeSerializedSize() { return 0; } + @Override + public void writeTo(ProtoSink output) {} + @Override + public Message mergeFrom(ProtoSource input) { return null; } + @Override + public boolean equals(Object obj) { return false; } + @Override + public Message clone() { return null; } + } + } + + @Logged + class Example { + ProtobufType x; // Should be logged + ProtobufType[] arr1; // Should not be logged + ProtobufType[][] arr2; // Should not be logged + List list; // Should not be logged + } + """; + + String expectedGeneratedSource = + """ + package edu.wpi.first.epilogue; + + import edu.wpi.first.epilogue.Logged; + import edu.wpi.first.epilogue.Epilogue; + import edu.wpi.first.epilogue.logging.ClassSpecificLogger; + import edu.wpi.first.epilogue.logging.EpilogueBackend; + + public class ExampleLogger extends ClassSpecificLogger { + public ExampleLogger() { + super(Example.class); + } + + @Override + public void update(EpilogueBackend backend, Example object) { + if (Epilogue.shouldLog(Logged.Importance.DEBUG)) { + backend.log("x", object.x, edu.wpi.first.epilogue.ProtobufType.proto); + } + } + } + """; + + assertLoggerGenerates(source, expectedGeneratedSource); + } + @Test void lists() { String source = diff --git a/epilogue-runtime/BUILD.bazel b/epilogue-runtime/BUILD.bazel index 05db6b7527..921e36292a 100644 --- a/epilogue-runtime/BUILD.bazel +++ b/epilogue-runtime/BUILD.bazel @@ -8,5 +8,6 @@ java_library( "//ntcore:networktables-java", "//wpiunits", "//wpiutil:wpiutil-java", + "@maven//:us_hebi_quickbuf_quickbuf_runtime", ], ) diff --git a/epilogue-runtime/build.gradle b/epilogue-runtime/build.gradle index fb96095a0a..f4f8e05869 100644 --- a/epilogue-runtime/build.gradle +++ b/epilogue-runtime/build.gradle @@ -13,4 +13,5 @@ dependencies { api(project(':ntcore')) api(project(':wpiutil')) api(project(':wpiunits')) + testImplementation(project(':wpimath')) // for convenient protobuf types } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/EpilogueBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/EpilogueBackend.java index f006a319dc..f941b7ca6f 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/EpilogueBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/EpilogueBackend.java @@ -6,8 +6,10 @@ package edu.wpi.first.epilogue.logging; import edu.wpi.first.units.Measure; import edu.wpi.first.units.Unit; +import edu.wpi.first.util.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.Collection; +import us.hebi.quickbuf.ProtoMessage; /** A backend is a generic interface for Epilogue to log discrete data points. */ public interface EpilogueBackend { @@ -193,6 +195,17 @@ public interface EpilogueBackend { log(identifier, array, struct); } + /** + * Logs a protobuf-serializable object. + * + * @param identifier the identifier of the data point + * @param value the value of the data point + * @param proto the protobuf to use to serialize the data + * @param

the protobuf-serializable type + * @param the protobuf message type + */ + > void log(String identifier, P value, Protobuf proto); + /** * Logs a measurement's value in terms of its base unit. * diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/FileBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/FileBackend.java index 0a116a03d9..18cc41e361 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/FileBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/FileBackend.java @@ -16,17 +16,20 @@ 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.ProtobufLogEntry; 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.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; +import us.hebi.quickbuf.ProtoMessage; /** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */ public class FileBackend implements EpilogueBackend { @@ -34,6 +37,7 @@ public class FileBackend implements EpilogueBackend { private final Map m_entries = new HashMap<>(); private final Map m_subLoggers = new HashMap<>(); private final Set> m_seenSchemas = new HashSet<>(); + private final Set> m_seenProtos = new HashSet<>(); /** * Creates a new file-based backend. @@ -166,4 +170,19 @@ public class FileBackend implements EpilogueBackend { ((StructArrayLogEntry) m_entries.get(identifier)).append(value); } + + @Override + @SuppressWarnings("unchecked") + public > void log(String identifier, P value, Protobuf proto) { + // DataLog.addSchema has checks that we're able to skip, avoiding allocations + if (m_seenProtos.add(proto)) { + m_dataLog.addSchema(proto); + } + + if (!m_entries.containsKey(identifier)) { + m_entries.put(identifier, ProtobufLogEntry.create(m_dataLog, identifier, proto)); + } + + ((ProtobufLogEntry

) m_entries.get(identifier)).append(value); + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/LazyBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/LazyBackend.java index bd925165e0..34938e2d98 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/LazyBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/LazyBackend.java @@ -4,11 +4,13 @@ package edu.wpi.first.epilogue.logging; +import edu.wpi.first.util.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import us.hebi.quickbuf.ProtoMessage; /** * A backend implementation that only logs data when it changes. Useful for keeping bandwidth and @@ -243,4 +245,17 @@ public class LazyBackend implements EpilogueBackend { m_previousValues.put(identifier, value.clone()); m_backend.log(identifier, value, struct); } + + @Override + public > void log(String identifier, P value, Protobuf proto) { + var previous = m_previousValues.get(identifier); + + if (Objects.equals(previous, value)) { + // no change + return; + } + + m_previousValues.put(identifier, value); + m_backend.log(identifier, value, proto); + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/MultiBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/MultiBackend.java index 575fde05b2..c6710a53eb 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/MultiBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/MultiBackend.java @@ -4,10 +4,12 @@ package edu.wpi.first.epilogue.logging; +import edu.wpi.first.util.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.HashMap; import java.util.List; import java.util.Map; +import us.hebi.quickbuf.ProtoMessage; /** * A backend implementation that delegates to other backends. Helpful for simultaneous logging to @@ -137,4 +139,11 @@ public class MultiBackend implements EpilogueBackend { backend.log(identifier, value, struct); } } + + @Override + public > void log(String identifier, P value, Protobuf proto) { + for (EpilogueBackend backend : m_backends) { + backend.log(identifier, value, proto); + } + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NTEpilogueBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NTEpilogueBackend.java index e398172e77..0af1b82440 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NTEpilogueBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NTEpilogueBackend.java @@ -13,18 +13,21 @@ 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.ProtobufPublisher; 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.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Function; +import us.hebi.quickbuf.ProtoMessage; /** * A backend implementation that sends data over network tables. Be careful when using this, since @@ -36,6 +39,7 @@ public class NTEpilogueBackend implements EpilogueBackend { private final Map m_publishers = new HashMap<>(); private final Map m_nestedBackends = new HashMap<>(); private final Set> m_seenSchemas = new HashSet<>(); + private final Set> m_seenProtos = new HashSet<>(); private final Function m_createIntPublisher; private final Function m_createFloatPublisher; private final Function m_createDoublePublisher; @@ -198,4 +202,21 @@ public class NTEpilogueBackend implements EpilogueBackend { publisher.set(value); } } + + @Override + @SuppressWarnings("unchecked") + public > void log(String identifier, P value, Protobuf proto) { + // NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations + if (m_seenProtos.add(proto)) { + m_nt.addSchema(proto); + } + + if (m_publishers.containsKey(identifier)) { + ((ProtobufPublisher

) m_publishers.get(identifier)).set(value); + } else { + ProtobufPublisher

publisher = m_nt.getProtobufTopic(identifier, proto).publish(); + m_publishers.put(identifier, publisher); + publisher.set(value); + } + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NestedBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NestedBackend.java index f288566085..e256b81172 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NestedBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NestedBackend.java @@ -4,9 +4,11 @@ package edu.wpi.first.epilogue.logging; +import edu.wpi.first.util.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; import java.util.HashMap; import java.util.Map; +import us.hebi.quickbuf.ProtoMessage; /** * A backend that logs to an underlying backend, prepending all logged data with a specific prefix. @@ -147,4 +149,9 @@ public class NestedBackend implements EpilogueBackend { public void log(String identifier, S[] value, Struct struct) { m_impl.log(withPrefix(identifier), value, struct); } + + @Override + public > void log(String identifier, P value, Protobuf proto) { + m_impl.log(m_prefix + identifier, value, proto); + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NullBackend.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NullBackend.java index e78ca0191d..8a2c52f2b6 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NullBackend.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/logging/NullBackend.java @@ -4,7 +4,9 @@ package edu.wpi.first.epilogue.logging; +import edu.wpi.first.util.protobuf.Protobuf; import edu.wpi.first.util.struct.Struct; +import us.hebi.quickbuf.ProtoMessage; /** Null backend implementation that logs nothing. */ public class NullBackend implements EpilogueBackend { @@ -62,4 +64,8 @@ public class NullBackend implements EpilogueBackend { @Override public void log(String identifier, S[] value, Struct struct) {} + + @Override + public > void log( + String identifier, P value, Protobuf proto) {} } diff --git a/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/LazyBackendTest.java b/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/LazyBackendTest.java index d0b394330c..39525d8a9f 100644 --- a/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/LazyBackendTest.java +++ b/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/LazyBackendTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import edu.wpi.first.math.geometry.Rotation2d; import java.util.List; import org.junit.jupiter.api.Test; @@ -185,4 +186,17 @@ class LazyBackendTest { assertArrayEquals( new byte[] {0x01, 0x00, 0x00, 0x00}, (byte[]) backend.getEntries().get(1).value()); } + + @Test + void lazyProtobuf() { + var backend = new TestBackend(); + var lazy = new LazyBackend(backend); + + var rotation = Rotation2d.kZero; + lazy.log("rotation", rotation, Rotation2d.proto); + assertEquals(1, backend.getEntries().size()); + var entry = backend.getEntries().get(0); + assertEquals("rotation", entry.identifier()); + assertArrayEquals(new byte[] {9, 0, 0, 0, 0, 0, 0, 0, 0}, (byte[]) entry.value()); + } } diff --git a/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/TestBackend.java b/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/TestBackend.java index 1372921002..90c06b465a 100644 --- a/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/TestBackend.java +++ b/epilogue-runtime/src/test/java/edu/wpi/first/epilogue/logging/TestBackend.java @@ -4,12 +4,14 @@ package edu.wpi.first.epilogue.logging; +import edu.wpi.first.util.protobuf.Protobuf; 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; +import us.hebi.quickbuf.ProtoMessage; @SuppressWarnings("PMD.TestClassWithoutTestCases") // This is not a test class! public class TestBackend implements EpilogueBackend { @@ -114,4 +116,12 @@ public class TestBackend implements EpilogueBackend { m_entries.add(new LogEntry<>(identifier, serialized)); } + + @Override + public > void log(String identifier, P value, Protobuf proto) { + var msg = proto.createMessage(); + proto.pack(msg, value); + var serialized = msg.toByteArray(); + m_entries.add(new LogEntry<>(identifier, serialized)); + } }