From ee0a8a1e568787613918db81f8737869da37d1d0 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Sat, 20 Sep 2025 14:23:22 -0400 Subject: [PATCH] [epilogue] Support logging of protobuf-serializable types (#8229) For parity with struct-serializable types. Change struct serialization to only apply to types with a public static final struct field, instead of relying only on the marker interface (which is not always followed). Doing this allows fallthrough to the protobuf handler for types with dynamic structs but static protobuf serializers. --- .../processor/AnnotationProcessor.java | 3 +- .../epilogue/processor/ProtobufHandler.java | 102 ++++++++++++++++++ .../epilogue/processor/StructHandler.java | 56 +++++++++- .../processor/AnnotationProcessorTest.java | 70 ++++++++++++ epilogue-runtime/BUILD.bazel | 1 + epilogue-runtime/build.gradle | 1 + .../epilogue/logging/EpilogueBackend.java | 13 +++ .../first/epilogue/logging/FileBackend.java | 19 ++++ .../first/epilogue/logging/LazyBackend.java | 15 +++ .../first/epilogue/logging/MultiBackend.java | 9 ++ .../epilogue/logging/NTEpilogueBackend.java | 21 ++++ .../first/epilogue/logging/NestedBackend.java | 7 ++ .../first/epilogue/logging/NullBackend.java | 6 ++ .../epilogue/logging/LazyBackendTest.java | 14 +++ .../first/epilogue/logging/TestBackend.java | 10 ++ 15 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ProtobufHandler.java 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)); + } }