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;
+ }
+ }
+}