Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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/}).
*
* <p>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).
*
* <p>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<String, LogValue> 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);
}
}
7 changes: 5 additions & 2 deletions src/main/java/xbot/common/command/BaseRobot.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ public boolean isSetToDefault() {
@Override
public void refreshDataFrame() {
currentValue = get_internal();
Logger.processInputs(prefix, inputs);
Logger.processInputs(akitLogPrefix(), inputs);
}
}
2 changes: 1 addition & 1 deletion src/main/java/xbot/common/properties/DoubleProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@ public boolean isSetToDefault() {
@Override
public void refreshDataFrame() {
currentValue = get_internal();
Logger.processInputs(prefix, inputs);
Logger.processInputs(akitLogPrefix(), inputs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ public boolean isSetToDefault() {
@Override
public void refreshDataFrame() {
currentValue.mut_replace(get_internal());
Logger.processInputs(prefix, inputs);
Logger.processInputs(akitLogPrefix(), inputs);
}
}
27 changes: 27 additions & 0 deletions src/main/java/xbot/common/properties/Property.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@
* @author Alex
*/
public abstract class Property implements DataFrameRefreshable {
/**
* Namespace used for the AdvantageKit log subtable that Property values flow through.
* <p>
* 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).
* <p>
* 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.
*/
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/xbot/common/properties/StringProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ public boolean isSetToDefault() {
@Override
public void refreshDataFrame() {
currentValue = get_internal();
Logger.processInputs(prefix, inputs);
Logger.processInputs(akitLogPrefix(), inputs);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, LogValue> 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<String, LogValue> 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<String, LogValue> 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<String, LogValue> 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;
}
}
}