From 761e57cce546245a08741f3116a35eae92928db8 Mon Sep 17 00:00:00 2001 From: reecelikesramen <3149587+reecelikesramen@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:58:26 -0600 Subject: [PATCH] Add Observable pattern to LoggedNetworkInputs * extend the LoggedNetworkInputs ABC with ObservableLoggedNetworkInputs ABC * Make concrete LoggedNetwork classes implement the Observable ABC * Uses weak references for listeners * Reads change queue with network tables and notifies on change --- .../networktables/LoggedDashboardChooser.java | 33 ++++---- .../networktables/LoggedNetworkBoolean.java | 29 ++++++- .../networktables/LoggedNetworkNumber.java | 28 ++++++- .../networktables/LoggedNetworkString.java | 26 +++++- .../ObservableLoggedNetworkInput.java | 81 +++++++++++++++++++ 5 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 akit/src/main/java/org/littletonrobotics/junction/networktables/ObservableLoggedNetworkInput.java diff --git a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedDashboardChooser.java b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedDashboardChooser.java index 9bee38fd..c6816824 100644 --- a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedDashboardChooser.java +++ b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedDashboardChooser.java @@ -13,21 +13,20 @@ package org.littletonrobotics.junction.networktables; +import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; - import org.littletonrobotics.junction.LogTable; import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.inputs.LoggableInputs; -import edu.wpi.first.wpilibj.smartdashboard.SendableChooser; -import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; - /** * Manages a chooser value published to the "SmartDashboard" table of NT. */ public class LoggedDashboardChooser extends LoggedNetworkInput { + private final String key; private String selectedValue = null; private SendableChooser sendableChooser = new SendableChooser<>(); @@ -70,30 +69,34 @@ public LoggedDashboardChooser(String key) { @SuppressWarnings("unchecked") public LoggedDashboardChooser(String key, SendableChooser chooser) { this(key); - // Get options map Map options = new HashMap<>(); try { Field mapField = SendableChooser.class.getDeclaredField("m_map"); mapField.setAccessible(true); options = (Map) mapField.get(chooser); - } catch (NoSuchFieldException - | SecurityException - | IllegalArgumentException - | IllegalAccessException e) { + } catch ( + NoSuchFieldException + | SecurityException + | IllegalArgumentException + | IllegalAccessException e + ) { e.printStackTrace(); } // Get default option String defaultString = ""; try { - Field defaultField = SendableChooser.class.getDeclaredField("m_defaultChoice"); + Field defaultField = + SendableChooser.class.getDeclaredField("m_defaultChoice"); defaultField.setAccessible(true); defaultString = (String) defaultField.get(chooser); - } catch (NoSuchFieldException - | SecurityException - | IllegalArgumentException - | IllegalAccessException e) { + } catch ( + NoSuchFieldException + | SecurityException + | IllegalArgumentException + | IllegalAccessException e + ) { e.printStackTrace(); } @@ -142,4 +145,4 @@ public void periodic() { } Logger.processInputs(prefix + "/SmartDashboard", inputs); } -} \ No newline at end of file +} diff --git a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkBoolean.java b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkBoolean.java index ad01df50..a8347a4c 100644 --- a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkBoolean.java +++ b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkBoolean.java @@ -15,15 +15,21 @@ import edu.wpi.first.networktables.BooleanEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.PubSubOption; +import edu.wpi.first.wpilibj.DriverStation; import org.littletonrobotics.junction.LogTable; import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.inputs.LoggableInputs; /** Manages a boolean value published to the root table of NT. */ -public class LoggedNetworkBoolean extends LoggedNetworkInput { +public class LoggedNetworkBoolean + extends ObservableLoggedNetworkInput { + + private static final boolean DEFAULT_VALUE = false; + private final String key; private final BooleanEntry entry; - private boolean defaultValue = false; + private boolean defaultValue = DEFAULT_VALUE; private boolean value; /** @@ -34,8 +40,15 @@ public class LoggedNetworkBoolean extends LoggedNetworkInput { * "/DashboardInputs/{key}" when logged. */ public LoggedNetworkBoolean(String key) { + super(DEFAULT_VALUE); this.key = key; - this.entry = NetworkTableInstance.getDefault().getBooleanTopic(key).getEntry(false); + this.entry = NetworkTableInstance.getDefault() + .getBooleanTopic(key) + .getEntry( + DEFAULT_VALUE, + PubSubOption.keepDuplicates(false), + PubSubOption.pollStorage(1) + ); this.value = defaultValue; Logger.registerDashboardInput(this); } @@ -69,7 +82,7 @@ public void set(boolean value) { } /** Returns the current value. */ - public boolean get() { + public Boolean get() { return value; } @@ -86,6 +99,14 @@ public void fromLog(LogTable table) { public void periodic() { if (!Logger.hasReplaySource()) { value = entry.get(defaultValue); + + // Only do tunables when not in a match + if (DriverStation.getMatchType() == DriverStation.MatchType.None) { + var changes = entry.readQueueValues(); + if (changes.length == 1) { + notifyListeners(); + } + } } Logger.processInputs(prefix, inputs); } diff --git a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkNumber.java b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkNumber.java index 81459bab..2dfa1b8b 100644 --- a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkNumber.java +++ b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkNumber.java @@ -15,15 +15,20 @@ import edu.wpi.first.networktables.DoubleEntry; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.PubSubOption; +import edu.wpi.first.wpilibj.DriverStation; import org.littletonrobotics.junction.LogTable; import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.inputs.LoggableInputs; /** Manages a number value published to the root table of NT. */ -public class LoggedNetworkNumber extends LoggedNetworkInput { +public class LoggedNetworkNumber extends ObservableLoggedNetworkInput { + + private static final double DEFAULT_VALUE = 0.0; + private final String key; private final DoubleEntry entry; - private double defaultValue = 0.0; + private double defaultValue = DEFAULT_VALUE; private double value; /** @@ -34,8 +39,15 @@ public class LoggedNetworkNumber extends LoggedNetworkInput { * "/DashboardInputs/{key}" when logged. */ public LoggedNetworkNumber(String key) { + super(DEFAULT_VALUE); this.key = key; - this.entry = NetworkTableInstance.getDefault().getDoubleTopic(key).getEntry(0.0); + this.entry = NetworkTableInstance.getDefault() + .getDoubleTopic(key) + .getEntry( + DEFAULT_VALUE, + PubSubOption.keepDuplicates(false), + PubSubOption.pollStorage(1) + ); this.value = defaultValue; Logger.registerDashboardInput(this); } @@ -69,7 +81,7 @@ public void set(double value) { } /** Returns the current value. */ - public double get() { + public Double get() { return value; } @@ -86,6 +98,14 @@ public void fromLog(LogTable table) { public void periodic() { if (!Logger.hasReplaySource()) { value = entry.get(defaultValue); + + // Only do tunables when not in a match + if (DriverStation.getMatchType() == DriverStation.MatchType.None) { + var changes = entry.readQueueValues(); + if (changes.length == 1) { + notifyListeners(); + } + } } Logger.processInputs(prefix, inputs); } diff --git a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkString.java b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkString.java index 4bfa4c4e..629e1df1 100644 --- a/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkString.java +++ b/akit/src/main/java/org/littletonrobotics/junction/networktables/LoggedNetworkString.java @@ -14,16 +14,21 @@ package org.littletonrobotics.junction.networktables; import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.PubSubOption; import edu.wpi.first.networktables.StringEntry; +import edu.wpi.first.wpilibj.DriverStation; import org.littletonrobotics.junction.LogTable; import org.littletonrobotics.junction.Logger; import org.littletonrobotics.junction.inputs.LoggableInputs; /** Manages a String value published to the root table of NT. */ -public class LoggedNetworkString extends LoggedNetworkInput { +public class LoggedNetworkString extends ObservableLoggedNetworkInput { + + private static final String DEFAULT_VALUE = ""; + private final String key; private final StringEntry entry; - private String defaultValue = ""; + private String defaultValue = DEFAULT_VALUE; private String value; /** @@ -34,8 +39,15 @@ public class LoggedNetworkString extends LoggedNetworkInput { * "/DashboardInputs/{key}" when logged. */ public LoggedNetworkString(String key) { + super(DEFAULT_VALUE); this.key = key; - this.entry = NetworkTableInstance.getDefault().getStringTopic(key).getEntry(""); + this.entry = NetworkTableInstance.getDefault() + .getStringTopic(key) + .getEntry( + DEFAULT_VALUE, + PubSubOption.keepDuplicates(false), + PubSubOption.pollStorage(1) + ); this.value = defaultValue; Logger.registerDashboardInput(this); } @@ -86,6 +98,14 @@ public void fromLog(LogTable table) { public void periodic() { if (!Logger.hasReplaySource()) { value = entry.get(defaultValue); + + // Only do tunables when not in a match + if (DriverStation.getMatchType() == DriverStation.MatchType.None) { + var changes = entry.readQueueValues(); + if (changes.length == 1) { + notifyListeners(); + } + } } Logger.processInputs(prefix, inputs); } diff --git a/akit/src/main/java/org/littletonrobotics/junction/networktables/ObservableLoggedNetworkInput.java b/akit/src/main/java/org/littletonrobotics/junction/networktables/ObservableLoggedNetworkInput.java new file mode 100644 index 00000000..c9eb71b4 --- /dev/null +++ b/akit/src/main/java/org/littletonrobotics/junction/networktables/ObservableLoggedNetworkInput.java @@ -0,0 +1,81 @@ +// Copyright 2021-2025 FRC 6328 +// http://github.com/Mechanical-Advantage +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// version 3 as published by the Free Software Foundation or +// available in the root directory of this project. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +package org.littletonrobotics.junction.networktables; + +import edu.wpi.first.wpilibj.DriverStation; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +public abstract class ObservableLoggedNetworkInput + extends LoggedNetworkInput { + + public static final String prefix = "NetworkInputs"; + private final T defaultValue; + private final List>> listeners; + + protected ObservableLoggedNetworkInput(T defaultValue) { + this.defaultValue = defaultValue; + this.listeners = new ArrayList<>(); + } + + public abstract T get(); + + /** Removes the leading slash from a key. */ + protected static String removeSlash(String key) { + if (key.startsWith("/")) { + return key.substring(1); + } else { + return key; + } + } + + public void addListener(Consumer listener) { + listeners.add(new WeakReference<>(listener)); + + // Only do tunables when not in a match, when in a match, give listener the + // default value + if (DriverStation.getMatchType() == DriverStation.MatchType.None) { + listener.accept(get()); + } else { + listener.accept(defaultValue); + } + } + + public void removeListener(Consumer listener) { + Iterator>> iter = listeners.iterator(); + while (iter.hasNext()) { + WeakReference> ref = iter.next(); + Consumer current = ref.get(); + if (current == null || current == listener) { + iter.remove(); + } + } + } + + protected void notifyListeners() { + Iterator>> iter = listeners.iterator(); + while (iter.hasNext()) { + WeakReference> ref = iter.next(); + Consumer listener = ref.get(); + if (listener != null) { + listener.accept(get()); + } else { + iter.remove(); + } + } + } +}