From 94fb68feab32d79aa8bd77821f9482695f998ce4 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Fri, 22 Apr 2016 23:57:34 -0700 Subject: [PATCH] Add NetworkTable Source. This reads a boolean, number, or string value from a NetworkTable key and makes it available to GRIP. --- .../edu/wpi/grip/core/GRIPCoreModule.java | 4 + .../main/java/edu/wpi/grip/core/Source.java | 4 + .../wpi/grip/core/sockets/SocketHints.java | 7 + .../grip/core/sources/NetworkValueSource.java | 199 ++++++++++++++++++ .../wpi/grip/ui/pipeline/AddSourceView.java | 59 +++++- 5 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/edu/wpi/grip/core/sources/NetworkValueSource.java diff --git a/core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java b/core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java index c4869ad969..97eb263fa7 100644 --- a/core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java +++ b/core/src/main/java/edu/wpi/grip/core/GRIPCoreModule.java @@ -15,6 +15,7 @@ import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkValueSource; import edu.wpi.grip.core.util.ExceptionWitness; import edu.wpi.grip.core.util.GRIPMode; @@ -119,6 +120,9 @@ public void hear(TypeLiteral type, TypeEncounter encounter) { install(new FactoryModuleBuilder() .implement(MultiImageFileSource.class, MultiImageFileSource.class) .build(MultiImageFileSource.Factory.class)); + install(new FactoryModuleBuilder() + .implement(NetworkValueSource.class, NetworkValueSource.class) + .build(NetworkValueSource.Factory.class)); install(new FactoryModuleBuilder().build(ExceptionWitness.Factory.class)); } diff --git a/core/src/main/java/edu/wpi/grip/core/Source.java b/core/src/main/java/edu/wpi/grip/core/Source.java index d8d46ac91f..df32d28407 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -6,6 +6,7 @@ import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkValueSource; import edu.wpi.grip.core.util.ExceptionWitness; import java.io.IOException; @@ -28,12 +29,15 @@ public static class SourceFactoryImpl implements SourceFactory { ImageFileSource.Factory imageFactory; @Inject MultiImageFileSource.Factory multiImageFactory; + @Inject + NetworkValueSource.Factory networkValueFactory; @Override public Source create(Class type, Properties properties) throws IOException { if (type.isAssignableFrom(CameraSource.class)) return cameraFactory.create(properties); else if (type.isAssignableFrom(ImageFileSource.class)) return imageFactory.create(properties); else if (type.isAssignableFrom(MultiImageFileSource.class)) return multiImageFactory.create(properties); + else if (type.isAssignableFrom(NetworkValueSource.class)) return networkValueFactory.create(properties); else throw new IllegalArgumentException(type + " was not a valid type"); } } diff --git a/core/src/main/java/edu/wpi/grip/core/sockets/SocketHints.java b/core/src/main/java/edu/wpi/grip/core/sockets/SocketHints.java index 9bfad9520e..9ef1609bf6 100644 --- a/core/src/main/java/edu/wpi/grip/core/sockets/SocketHints.java +++ b/core/src/main/java/edu/wpi/grip/core/sockets/SocketHints.java @@ -92,6 +92,13 @@ public static SocketHint createBooleanSocketHint(final String identifie public static SocketHint createNumberSocketHint(final String identifier, Number defaultValue) { return createNumberSocketHintBuilder(identifier, defaultValue).build(); } + + public static SocketHint createTextSocketHint(final String identifier, final String defaultValue) { + return new SocketHint.Builder<>(String.class) + .identifier(identifier) + .initialValue(defaultValue) + .build(); + } } public static > SocketHint createEnumSocketHint(final String identifier, diff --git a/core/src/main/java/edu/wpi/grip/core/sources/NetworkValueSource.java b/core/src/main/java/edu/wpi/grip/core/sources/NetworkValueSource.java new file mode 100644 index 0000000000..c88738dd7a --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/sources/NetworkValueSource.java @@ -0,0 +1,199 @@ +package edu.wpi.grip.core.sources; + + +import com.google.common.base.StandardSystemProperty; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import com.thoughtworks.xstream.annotations.XStreamAlias; +import edu.wpi.first.wpilibj.networktables.NetworkTable; +import edu.wpi.first.wpilibj.tables.ITable; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.SocketHint; +import edu.wpi.grip.core.sockets.SocketHints; +import edu.wpi.grip.core.Source; +import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; +import edu.wpi.grip.core.util.ExceptionWitness; +import edu.wpi.grip.core.util.service.LoggingListener; + +import java.io.IOException; +import java.util.Optional; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Provides a way to get a value from NetworkTables. + */ +@XStreamAlias(value = "grip:NetworkValue") +public class NetworkValueSource extends Source { + + private final static String KEY_PROPERTY = "key"; + private final static String TYPE_PROPERTY = "type"; + private static final Logger logger = Logger.getLogger(NetworkValueSource.class.getName()); + + private final String name; + + private final EventBus eventBus; + private final Properties properties; + + private final SocketHint numberOutputHint = SocketHints.createNumberSocketHint("Value", 0); + private final SocketHint booleanOutputHint = SocketHints.createBooleanSocketHint("Value", false); + private final SocketHint stringOutputHint = SocketHints.Outputs.createTextSocketHint("Value", ""); + private final OutputSocket numberOutputSocket; + private final OutputSocket booleanOutputSocket; + private final OutputSocket stringOutputSocket; + private final OutputSocket outputSocket; + private final String key; + private volatile boolean booleanPrevValue = false; + private volatile double numberPrevValue = 0; + private volatile String stringPrevValue = ""; + private ITable table = NetworkTable.getTable("GRIP"); + + public enum ValueType { + BOOLEAN, + NUMBER, + STRING, + } + private final ValueType valueType; + + public interface Factory { + NetworkValueSource create(String key, ValueType valueType) throws IOException; + + NetworkValueSource create(Properties properties) throws IOException; + } + + /** + * Creates a camera source that can be used as an input to a pipeline + * + * @param eventBus The EventBus to attach to + * @param address A URL to stream video from an IP camera + */ + @AssistedInject + NetworkValueSource( + final EventBus eventBus, + final ExceptionWitness.Factory exceptionWitnessFactory, + @Assisted final String key, + @Assisted final ValueType valueType) throws IOException { + this(eventBus, exceptionWitnessFactory, createProperties(key, valueType)); + } + + /** + * Used for serialization + */ + @AssistedInject + NetworkValueSource( + final EventBus eventBus, + final ExceptionWitness.Factory exceptionWitnessFactory, + @Assisted final Properties properties) { + super(exceptionWitnessFactory); + this.eventBus = eventBus; + this.properties = properties; + + final String keyProperty = properties.getProperty(KEY_PROPERTY); + if (keyProperty == null) { + throw new IllegalArgumentException("Cannot initialize NetworkValueSource without a key"); + } + this.name = "Network Value " + keyProperty; + this.key = keyProperty; + + final String typeProperty = properties.getProperty(TYPE_PROPERTY); + if (typeProperty == null) { + throw new IllegalArgumentException("Cannot initialize NetworkValueSource without a type"); + } + if (typeProperty.equals("BOOLEAN")) { + this.booleanOutputSocket = new OutputSocket<>(eventBus, booleanOutputHint); + this.numberOutputSocket = null; + this.stringOutputSocket = null; + this.outputSocket = booleanOutputSocket; + this.valueType = ValueType.BOOLEAN; + } else if (typeProperty.equals("NUMBER")) { + this.booleanOutputSocket = null; + this.numberOutputSocket = new OutputSocket<>(eventBus, numberOutputHint); + this.stringOutputSocket = null; + this.outputSocket = numberOutputSocket; + this.valueType = ValueType.NUMBER; + } else if (typeProperty.equals("STRING")) { + this.booleanOutputSocket = null; + this.numberOutputSocket = null; + this.stringOutputSocket = new OutputSocket<>(eventBus, stringOutputHint); + this.outputSocket = stringOutputSocket; + this.valueType = ValueType.STRING; + } else { + throw new IllegalArgumentException("NetworkValueSource does not support type " + typeProperty); + } + + table.addTableListener(key, (source, key, value, isNew) -> { + eventBus.post(new SourceHasPendingUpdateEvent(this)); + }, true); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public OutputSocket[] createOutputSockets() { + return new OutputSocket[]{outputSocket}; + } + + @Override + protected boolean updateOutputSockets() { + if (valueType == ValueType.BOOLEAN) { + boolean newValue = table.getBoolean(key, false); + // We have a new value then we need to update the socket value + if (newValue != booleanPrevValue) { + booleanPrevValue = newValue; + // Update the value + booleanOutputSocket.setValue(newValue); + // We have updated output sockets + return true; + } else { + return false; // No output sockets were updated + } + } else if (valueType == ValueType.NUMBER) { + double newValue = table.getNumber(key, 0); + // We have a new value then we need to update the socket value + if (newValue != numberPrevValue) { + numberPrevValue = newValue; + // Update the value + numberOutputSocket.setValue(newValue); + // We have updated output sockets + return true; + } else { + return false; // No output sockets were updated + } + } else if (valueType == ValueType.STRING) { + String newValue = table.getString(key, ""); + // We have a new value then we need to update the socket value + if (!newValue.equals(stringPrevValue)) { + stringPrevValue = newValue; + // Update the value + stringOutputSocket.setValue(newValue); + // We have updated output sockets + return true; + } else { + return false; // No output sockets were updated + } + } else { + return false; + } + } + + @Override + public Properties getProperties() { + return this.properties; + } + + @Override + public void initialize() { + } + + private static Properties createProperties(String key, ValueType valueType) { + final Properties properties = new Properties(); + properties.setProperty(KEY_PROPERTY, key); + properties.setProperty(TYPE_PROPERTY, valueType.toString()); + return properties; + } +} diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java index ce506a2ab9..a00d5a6aa2 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java @@ -9,9 +9,11 @@ import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkValueSource; import edu.wpi.grip.ui.util.DPIUtility; import edu.wpi.grip.ui.util.SupplierWithIO; import javafx.application.Platform; +import javafx.collections.FXCollections; import javafx.event.EventHandler; import javafx.scene.Parent; import javafx.scene.control.*; @@ -48,6 +50,7 @@ public class AddSourceView extends HBox { private final MultiImageFileSource.Factory multiImageSourceFactory; private final ImageFileSource.Factory imageSourceFactory; private final CameraSource.Factory cameraSourceFactory; + private final NetworkValueSource.Factory networkValueSourceFactory; private final Button webcamButton; private final Button ipcamButton; @@ -57,17 +60,27 @@ private static class SourceDialog extends Dialog { private final Text errorText = new Text(); private SourceDialog(final Parent root, Control inputField) { + this(root, inputField, null); + } + + private SourceDialog(final Parent root, Control inputField1, Control inputField2) { super(); this.getDialogPane().getStyleClass().add(SOURCE_DIALOG_STYLE_CLASS); final GridPane gridContent = new GridPane(); gridContent.setMaxWidth(Double.MAX_VALUE); - GridPane.setHgrow(inputField, Priority.ALWAYS); + if (inputField2 != null) { + GridPane.setHgrow(inputField2, Priority.ALWAYS); + } + GridPane.setHgrow(inputField1, Priority.ALWAYS); GridPane.setHgrow(errorText, Priority.NEVER); - errorText.wrappingWidthProperty().bind(inputField.widthProperty()); + errorText.wrappingWidthProperty().bind(inputField1.widthProperty()); gridContent.add(errorText, 0, 0); - gridContent.add(inputField, 0, 1); + gridContent.add(inputField1, 0, 1); + if (inputField2 != null) { + gridContent.add(inputField2, 0, 2); + } getDialogPane().setContent(gridContent); getDialogPane().setStyle(root.getStyle()); @@ -84,11 +97,13 @@ public interface Factory { AddSourceView(EventBus eventBus, MultiImageFileSource.Factory multiImageSourceFactory, ImageFileSource.Factory imageSourceFactory, - CameraSource.Factory cameraSourceFactory) { + CameraSource.Factory cameraSourceFactory, + NetworkValueSource.Factory networkValueSourceFactory) { this.eventBus = eventBus; this.multiImageSourceFactory = multiImageSourceFactory; this.imageSourceFactory = imageSourceFactory; this.cameraSourceFactory = cameraSourceFactory; + this.networkValueSourceFactory = networkValueSourceFactory; this.setFillHeight(true); @@ -201,6 +216,42 @@ public interface Factory { dialog.errorText.setText(e.getMessage()); }); }); + + addButton("Add NT\nValue", getClass().getResource("/edu/wpi/grip/ui/icons/add-webcam.png"), mouseEvent -> { + final Parent root = this.getScene().getRoot(); + + // Show a dialog for the user to pick a NetworkTable key + + final ChoiceBox type = new ChoiceBox(FXCollections.observableArrayList("Boolean", "Number", "String")); + final TextField key = new TextField(); + final SourceDialog dialog = new SourceDialog(root, key, type); + key.setPromptText("Key"); + + dialog.setTitle("Add NetworkTable Value"); + dialog.setHeaderText("Enter the NetworkTable type and key (under GRIP/)"); + + // If the user clicks OK, add a new source + activeDialog = Optional.of(dialog); + dialog.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).ifPresent(result -> { + try { + final NetworkValueSource source; + if (type.getValue().equals("Boolean")) { + source = networkValueSourceFactory.create(key.getText(), NetworkValueSource.ValueType.BOOLEAN); + } else if (type.getValue().equals("Number")) { + source = networkValueSourceFactory.create(key.getText(), NetworkValueSource.ValueType.NUMBER); + } else if (type.getValue().equals("String")) { + source = networkValueSourceFactory.create(key.getText(), NetworkValueSource.ValueType.STRING); + } else { + throw new IOException("unrecognized type " + type.getValue()); + } + source.initialize(); + eventBus.post(new SourceAddedEvent(source)); + } catch (IOException e) { + dialog.errorText.setText(e.getMessage()); + } + }); + activeDialog = Optional.empty(); + }); } /**