[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 <type> 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.
This commit is contained in:
Sam Carlberg
2025-09-20 14:23:22 -04:00
committed by GitHub
parent 3dbdfa1839
commit ee0a8a1e56
15 changed files with 344 additions and 3 deletions

View File

@@ -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 <P> the protobuf-serializable type
* @param <M> the protobuf message type
*/
<P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto);
/**
* Logs a measurement's value in terms of its base unit.
*

View File

@@ -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<String, DataLogEntry> m_entries = new HashMap<>();
private final Map<String, NestedBackend> m_subLoggers = new HashMap<>();
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
/**
* Creates a new file-based backend.
@@ -166,4 +170,19 @@ public class FileBackend implements EpilogueBackend {
((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value);
}
@Override
@SuppressWarnings("unchecked")
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> 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<P>) m_entries.get(identifier)).append(value);
}
}

View File

@@ -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 <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> 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);
}
}

View File

@@ -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 <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
for (EpilogueBackend backend : m_backends) {
backend.log(identifier, value, proto);
}
}
}

View File

@@ -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<String, Publisher> m_publishers = new HashMap<>();
private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>();
private final Function<String, IntegerPublisher> m_createIntPublisher;
private final Function<String, FloatPublisher> m_createFloatPublisher;
private final Function<String, DoublePublisher> m_createDoublePublisher;
@@ -198,4 +202,21 @@ public class NTEpilogueBackend implements EpilogueBackend {
publisher.set(value);
}
}
@Override
@SuppressWarnings("unchecked")
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> 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<P>) m_publishers.get(identifier)).set(value);
} else {
ProtobufPublisher<P> publisher = m_nt.getProtobufTopic(identifier, proto).publish();
m_publishers.put(identifier, publisher);
publisher.set(value);
}
}
}

View File

@@ -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 <S> void log(String identifier, S[] value, Struct<S> struct) {
m_impl.log(withPrefix(identifier), value, struct);
}
@Override
public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
m_impl.log(m_prefix + identifier, value, proto);
}
}

View File

@@ -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 <S> void log(String identifier, S[] value, Struct<S> struct) {}
@Override
public <P, M extends ProtoMessage<M>> void log(
String identifier, P value, Protobuf<P, M> proto) {}
}