diff --git a/src/main/java/xbot/common/advantage/PropertySkippingNT4Publisher.java b/src/main/java/xbot/common/advantage/PropertySkippingNT4Publisher.java new file mode 100644 index 00000000..4c3ef321 --- /dev/null +++ b/src/main/java/xbot/common/advantage/PropertySkippingNT4Publisher.java @@ -0,0 +1,65 @@ +package xbot.common.advantage; + +import org.littletonrobotics.junction.LogDataReceiver; +import org.littletonrobotics.junction.LogTable; +import org.littletonrobotics.junction.LogTable.LogValue; +import org.littletonrobotics.junction.networktables.NT4Publisher; + +import java.util.Map; + +import xbot.common.properties.Property; + +/** + * A {@link LogDataReceiver} that wraps {@link NT4Publisher} and skips any LogTable entry + * under the {@link Property#AKIT_LOG_NAMESPACE} subtable (currently {@code PropertyMirror/}). + * + *

Property values are routed through that subtable so the on-disk WPILOG receiver still + * captures them (replay correctness), but the live NetworkTables surface stays clean — + * dashboards see the same values via WPILib's {@code /Preferences/...} table (which is + * the editable/savable surface and is untouched by this wrapper). + * + *

All other keys (subsystem telemetry, {@code aKitLog.record(...)} outputs, system stats, + * driver-station data, etc.) flow through unchanged. + */ +public class PropertySkippingNT4Publisher implements LogDataReceiver { + + /** LogTable keys have a leading slash; the namespace constant does not. */ + static final String DENY_PREFIX = "/" + Property.AKIT_LOG_NAMESPACE; + + private final LogDataReceiver wrapped; + + public PropertySkippingNT4Publisher() { + this(new NT4Publisher()); + } + + /** Visible for testing. */ + PropertySkippingNT4Publisher(LogDataReceiver wrapped) { + this.wrapped = wrapped; + } + + @Override + public void start() { + wrapped.start(); + } + + @Override + public void end() { + wrapped.end(); + } + + @Override + public void putTable(LogTable table) throws InterruptedException { + LogTable filtered = new LogTable(table.getTimestamp()); + for (Map.Entry entry : table.getAll(false).entrySet()) { + String key = entry.getKey(); + if (key.startsWith(DENY_PREFIX)) { + continue; + } + // LogTable.put prepends the root prefix "/" automatically, so strip the + // leading slash we got from getAll() to avoid producing "//Foo". + String relativeKey = key.startsWith("/") ? key.substring(1) : key; + filtered.put(relativeKey, entry.getValue()); + } + wrapped.putTable(filtered); + } +} diff --git a/src/main/java/xbot/common/command/BaseRobot.java b/src/main/java/xbot/common/command/BaseRobot.java index 49c5eb8b..426da53e 100644 --- a/src/main/java/xbot/common/command/BaseRobot.java +++ b/src/main/java/xbot/common/command/BaseRobot.java @@ -9,7 +9,6 @@ import org.littletonrobotics.junction.LoggedPowerDistribution; import org.littletonrobotics.junction.LoggedRobot; import org.littletonrobotics.junction.Logger; -import org.littletonrobotics.junction.networktables.NT4Publisher; import org.littletonrobotics.junction.wpilog.WPILOGReader; import org.littletonrobotics.junction.wpilog.WPILOGWriter; @@ -21,6 +20,7 @@ import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; import xbot.common.advantage.DataFrameRefreshable; +import xbot.common.advantage.PropertySkippingNT4Publisher; import xbot.common.controls.sensors.XTimer; import xbot.common.controls.sensors.XTimerImpl; import xbot.common.injection.DevicePolice; @@ -98,7 +98,10 @@ public void robotInit() { if (logDirectory.exists() && logDirectory.isDirectory() && logDirectory.canWrite()) { Logger.addDataReceiver(new WPILOGWriter("/U/logs")); // Log to a USB stick with label LOGSDRIVE plugged into the inner usb port } - Logger.addDataReceiver(new NT4Publisher()); // Publish data to NetworkTables + // Publish data to NetworkTables, but skip the AKit-side mirror of Property + // values (they're in the on-disk WPILOG for replay, and the editable surface + // for dashboards lives at /Preferences/... via WPILib Preferences, untouched). + Logger.addDataReceiver(new PropertySkippingNT4Publisher()); LoggedPowerDistribution.getInstance( PowerDistribution.kDefaultModule, PowerDistribution.ModuleType.kRev); // Log power distribution data from the configured module diff --git a/src/main/java/xbot/common/properties/BooleanProperty.java b/src/main/java/xbot/common/properties/BooleanProperty.java index ce9af9a9..4d4fd9d6 100644 --- a/src/main/java/xbot/common/properties/BooleanProperty.java +++ b/src/main/java/xbot/common/properties/BooleanProperty.java @@ -79,6 +79,6 @@ public boolean isSetToDefault() { @Override public void refreshDataFrame() { currentValue = get_internal(); - Logger.processInputs(prefix, inputs); + Logger.processInputs(akitLogPrefix(), inputs); } } diff --git a/src/main/java/xbot/common/properties/DoubleProperty.java b/src/main/java/xbot/common/properties/DoubleProperty.java index a61f7575..87129bd6 100644 --- a/src/main/java/xbot/common/properties/DoubleProperty.java +++ b/src/main/java/xbot/common/properties/DoubleProperty.java @@ -92,6 +92,6 @@ public boolean isSetToDefault() { @Override public void refreshDataFrame() { currentValue = get_internal(); - Logger.processInputs(prefix, inputs); + Logger.processInputs(akitLogPrefix(), inputs); } } diff --git a/src/main/java/xbot/common/properties/MeasureProperty.java b/src/main/java/xbot/common/properties/MeasureProperty.java index 016ecdfe..4cbfaebe 100644 --- a/src/main/java/xbot/common/properties/MeasureProperty.java +++ b/src/main/java/xbot/common/properties/MeasureProperty.java @@ -98,6 +98,6 @@ public boolean isSetToDefault() { @Override public void refreshDataFrame() { currentValue.mut_replace(get_internal()); - Logger.processInputs(prefix, inputs); + Logger.processInputs(akitLogPrefix(), inputs); } } diff --git a/src/main/java/xbot/common/properties/Property.java b/src/main/java/xbot/common/properties/Property.java index cc47061c..40af8948 100644 --- a/src/main/java/xbot/common/properties/Property.java +++ b/src/main/java/xbot/common/properties/Property.java @@ -17,6 +17,22 @@ * @author Alex */ public abstract class Property implements DataFrameRefreshable { + /** + * Namespace used for the AdvantageKit log subtable that Property values flow through. + *

+ * Every Property records its current value as a {@code LoggableInputs} via + * {@code Logger.processInputs(akitLogPrefix(), inputs)} so replay sees the value the robot + * actually used. The AKit-side mirror is then forwarded to NetworkTables by default — which + * is redundant, because dashboards already see the value via WPILib's {@code /Preferences/...} + * surface. Routing the Property log entries through this dedicated subtable lets the NT + * publisher drop them by prefix without touching the on-disk log receiver (which still gets + * everything, keeping replay correct). + *

+ * The name {@code PropertyMirror} (rather than just {@code Properties}) avoids confusion with + * the WPILib {@code Preferences} table that lives at {@code /Preferences/...} in NetworkTables. + */ + public static final String AKIT_LOG_NAMESPACE = "PropertyMirror/"; + /** * The key for the property. */ @@ -76,6 +92,17 @@ public PropertyLevel getLevel() { return level; } + /** + * @return The full prefix this Property uses when calling + * {@code Logger.processInputs(...)}. Subclasses should pass this to + * {@code Logger.processInputs} so their LogTable entries land under the + * {@link #AKIT_LOG_NAMESPACE Properties/} subtable rather than mixing in with + * subsystem telemetry. + */ + protected String akitLogPrefix() { + return AKIT_LOG_NAMESPACE + prefix; + } + /** * Updates the backing store for debug-level properties. Called by XPropertyManager * when the global "show all debug properties" flag changes. diff --git a/src/main/java/xbot/common/properties/StringProperty.java b/src/main/java/xbot/common/properties/StringProperty.java index 5880cc75..3771acfe 100644 --- a/src/main/java/xbot/common/properties/StringProperty.java +++ b/src/main/java/xbot/common/properties/StringProperty.java @@ -74,6 +74,6 @@ public boolean isSetToDefault() { @Override public void refreshDataFrame() { currentValue = get_internal(); - Logger.processInputs(prefix, inputs); + Logger.processInputs(akitLogPrefix(), inputs); } } diff --git a/src/test/java/xbot/common/advantage/PropertySkippingNT4PublisherTest.java b/src/test/java/xbot/common/advantage/PropertySkippingNT4PublisherTest.java new file mode 100644 index 00000000..a2707c53 --- /dev/null +++ b/src/test/java/xbot/common/advantage/PropertySkippingNT4PublisherTest.java @@ -0,0 +1,147 @@ +package xbot.common.advantage; + +import org.junit.Before; +import org.junit.Test; +import org.littletonrobotics.junction.LogDataReceiver; +import org.littletonrobotics.junction.LogTable; +import org.littletonrobotics.junction.LogTable.LogValue; + +import java.util.Map; + +import xbot.common.properties.Property; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class PropertySkippingNT4PublisherTest { + + private CapturingReceiver wrapped; + private PropertySkippingNT4Publisher publisher; + + @Before + public void setUp() { + wrapped = new CapturingReceiver(); + publisher = new PropertySkippingNT4Publisher(wrapped); + } + + @Test + public void nonPropertyKeysAreForwarded() throws InterruptedException { + LogTable input = new LogTable(123L); + input.put("RealOutputs/Drive/Pose", 3.14); + input.put("SystemStats/BatteryVoltage", 12.4); + input.put("Timestamp", 123L); + + publisher.putTable(input); + + Map forwarded = wrapped.lastTable.getAll(false); + assertNotNull(forwarded.get("/RealOutputs/Drive/Pose")); + assertNotNull(forwarded.get("/SystemStats/BatteryVoltage")); + assertNotNull(forwarded.get("/Timestamp")); + } + + @Test + public void propertyNamespacedKeysAreDropped() throws InterruptedException { + LogTable input = new LogTable(1L); + input.put(Property.AKIT_LOG_NAMESPACE + "ShooterSubsystem/VoltageRampTime", 0.2); + input.put(Property.AKIT_LOG_NAMESPACE + "DriveSubsystem/MaxSpeed", 4.0); + + publisher.putTable(input); + + Map forwarded = wrapped.lastTable.getAll(false); + assertNull(forwarded.get("/" + Property.AKIT_LOG_NAMESPACE + "ShooterSubsystem/VoltageRampTime")); + assertNull(forwarded.get("/" + Property.AKIT_LOG_NAMESPACE + "DriveSubsystem/MaxSpeed")); + assertTrue(forwarded.isEmpty()); + } + + @Test + public void mixOfPropertyAndNonPropertyKeysIsHandled() throws InterruptedException { + LogTable input = new LogTable(7L); + input.put("RealOutputs/Shooter/RPM", 6000.0); + input.put(Property.AKIT_LOG_NAMESPACE + "Shooter/VoltageRampTime", 0.2); + input.put("DriverStation/Alliance", "Red"); + input.put(Property.AKIT_LOG_NAMESPACE + "Drive/MaxSpeed", 4.0); + + publisher.putTable(input); + + Map forwarded = wrapped.lastTable.getAll(false); + assertNotNull(forwarded.get("/RealOutputs/Shooter/RPM")); + assertNotNull(forwarded.get("/DriverStation/Alliance")); + assertNull(forwarded.get("/" + Property.AKIT_LOG_NAMESPACE + "Shooter/VoltageRampTime")); + assertNull(forwarded.get("/" + Property.AKIT_LOG_NAMESPACE + "Drive/MaxSpeed")); + assertEquals(2, forwarded.size()); + } + + @Test + public void timestampIsPreserved() throws InterruptedException { + LogTable input = new LogTable(424242L); + input.put("Timestamp", 424242L); + + publisher.putTable(input); + + assertEquals(424242L, wrapped.lastTable.getTimestamp()); + } + + @Test + public void emptyInputDoesNotCrash() throws InterruptedException { + LogTable input = new LogTable(0L); + + publisher.putTable(input); + + assertNotNull(wrapped.lastTable); + assertTrue(wrapped.lastTable.getAll(false).isEmpty()); + } + + @Test + public void startAndEndAreForwarded() { + publisher.start(); + publisher.end(); + assertTrue(wrapped.started); + assertTrue(wrapped.ended); + } + + @Test + public void denyPrefixIncludesLeadingSlash() { + // Sanity check on the relationship between the namespace constant and the deny prefix. + // LogTable.getAll() returns keys with a leading slash; the namespace doesn't have one. + assertTrue(PropertySkippingNT4Publisher.DENY_PREFIX.startsWith("/")); + assertEquals("/" + Property.AKIT_LOG_NAMESPACE, PropertySkippingNT4Publisher.DENY_PREFIX); + } + + @Test + public void keyThatMerelyContainsTheNamespaceIsNotDropped() throws InterruptedException { + // A key like /RealOutputs/PropertyMirror/Foo happens to contain "PropertyMirror/" but + // isn't under the namespace — it must still be forwarded. + LogTable input = new LogTable(1L); + input.put("RealOutputs/" + Property.AKIT_LOG_NAMESPACE + "Foo", 1.0); + + publisher.putTable(input); + + Map forwarded = wrapped.lastTable.getAll(false); + assertNotNull(forwarded.get("/RealOutputs/" + Property.AKIT_LOG_NAMESPACE + "Foo")); + assertFalse(forwarded.isEmpty()); + } + + private static final class CapturingReceiver implements LogDataReceiver { + LogTable lastTable; + boolean started; + boolean ended; + + @Override + public void start() { + started = true; + } + + @Override + public void end() { + ended = true; + } + + @Override + public void putTable(LogTable table) { + lastTable = table; + } + } +}