diff --git a/.gitignore b/.gitignore index fc7b805616..366b7a981f 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,7 @@ bower_components **/generated */generated_tests /bin/ + +### NetworkTables +networktables.ini +networktables.ini.bak 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 5e52da585a..314e77170a 100644 --- a/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java +++ b/core/src/main/java/edu/wpi/grip/core/GripCoreModule.java @@ -11,6 +11,7 @@ import edu.wpi.grip.core.sources.HttpSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkTableEntrySource; import edu.wpi.grip.core.util.ExceptionWitness; import edu.wpi.grip.core.util.GripMode; @@ -144,6 +145,9 @@ public void hear(TypeLiteral type, TypeEncounter encounter) { install(new FactoryModuleBuilder() .implement(HttpSource.class, HttpSource.class) .build(HttpSource.Factory.class)); + install(new FactoryModuleBuilder() + .implement(NetworkTableEntrySource.class, NetworkTableEntrySource.class) + .build(NetworkTableEntrySource.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 7c2a929f8b..5c4e9087dc 100644 --- a/core/src/main/java/edu/wpi/grip/core/Source.java +++ b/core/src/main/java/edu/wpi/grip/core/Source.java @@ -5,6 +5,7 @@ import edu.wpi.grip.core.sources.HttpSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkTableEntrySource; import edu.wpi.grip.core.util.ExceptionWitness; import com.google.common.collect.ImmutableList; @@ -111,6 +112,8 @@ public static class SourceFactoryImpl implements SourceFactory { MultiImageFileSource.Factory multiImageFactory; @Inject HttpSource.Factory httpFactory; + @Inject + NetworkTableEntrySource.Factory networkTableEntryFactory; @Override public Source create(Class type, Properties properties) throws IOException { @@ -122,6 +125,8 @@ public Source create(Class type, Properties properties) throws IOException { return multiImageFactory.create(properties); } else if (type.isAssignableFrom(HttpSource.class)) { return httpFactory.create(properties); + } else if (type.isAssignableFrom(NetworkTableEntrySource.class)) { + return networkTableEntryFactory.create(properties); } else { throw new IllegalArgumentException(type + " was not a valid type"); } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/GripNetworkModule.java b/core/src/main/java/edu/wpi/grip/core/operations/network/GripNetworkModule.java index f38cab7ee2..f965528f34 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/network/GripNetworkModule.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/GripNetworkModule.java @@ -34,5 +34,10 @@ protected void configure() { bind(ROSNetworkPublisherFactory.class) .annotatedWith(Names.named("rosManager")) .to(ROSManager.class); + + // Network receiver bindings + bind(MapNetworkReceiverFactory.class) + .annotatedWith(Names.named("ntManager")) + .to(NTManager.class); } } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/MapNetworkReceiverFactory.java b/core/src/main/java/edu/wpi/grip/core/operations/network/MapNetworkReceiverFactory.java new file mode 100644 index 0000000000..4a717dba30 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/MapNetworkReceiverFactory.java @@ -0,0 +1,10 @@ +package edu.wpi.grip.core.operations.network; + + +/** + * A factory to create {@link NetworkReceiver NetworkRecievers}. + */ +@FunctionalInterface +public interface MapNetworkReceiverFactory { + NetworkReceiver create(String path); +} diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkReceiver.java b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkReceiver.java new file mode 100644 index 0000000000..a48a1fd3c6 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/NetworkReceiver.java @@ -0,0 +1,45 @@ +package edu.wpi.grip.core.operations.network; + +import java.util.function.Consumer; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Manages the interface between the {@link PublishAnnotatedOperation} and the actual network + * protocol implemented by a specific {@link Manager}. + */ +public abstract class NetworkReceiver implements AutoCloseable { + + protected final String path; + + /** + * Create a new NetworkReceiver with the specified path. + * + * @param path The path of the object to get + */ + public NetworkReceiver(String path) { + checkNotNull(path, "Path cannot be null"); + checkArgument(!path.isEmpty(), "Path cannot be an empty string"); + this.path = path; + } + + /** + * Get the value of the object. + * + * @return The value of this NetworkReceiver + */ + public abstract Object getValue(); + + /** + * Add a listener to the NetworkReceiver item. + * + * @param consumer The consumer to call when this item has a update + */ + public abstract void addListener(Consumer consumer); + + /** + * Close the network reciever. This should not throw an exception. + */ + public abstract void close(); +} diff --git a/core/src/main/java/edu/wpi/grip/core/operations/network/networktables/NTManager.java b/core/src/main/java/edu/wpi/grip/core/operations/network/networktables/NTManager.java index c0cc69ed56..4ffa01f351 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/network/networktables/NTManager.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/network/networktables/NTManager.java @@ -5,6 +5,8 @@ import edu.wpi.grip.core.operations.network.Manager; import edu.wpi.grip.core.operations.network.MapNetworkPublisher; import edu.wpi.grip.core.operations.network.MapNetworkPublisherFactory; +import edu.wpi.grip.core.operations.network.MapNetworkReceiverFactory; +import edu.wpi.grip.core.operations.network.NetworkReceiver; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.util.GripMode; @@ -19,13 +21,15 @@ import edu.wpi.first.wpilibj.tables.ITable; import java.io.File; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; - import javax.inject.Inject; import static com.google.common.base.Preconditions.checkNotNull; @@ -34,18 +38,18 @@ * This class encapsulates the way we map various settings to the global NetworkTables state. */ @Singleton -public class NTManager implements Manager, MapNetworkPublisherFactory { +public class NTManager implements Manager, MapNetworkPublisherFactory, MapNetworkReceiverFactory { /* * Nasty hack that is unavoidable because of how NetworkTables works. */ - private static final AtomicInteger publisherCount = new AtomicInteger(0); + private static final AtomicInteger count = new AtomicInteger(0); /** * Information from: https://github.com/PeterJohnson/ntcore/blob/master/src/Log.h and * https://github.com/PeterJohnson/ntcore/blob/e6054f543a6ab10aa27af6cace855da66d67ee44 * /include/ntcore_c.h#L39 */ - private static final Map ntLogLevels = ImmutableMap.builder() + protected static final Map ntLogLevels = ImmutableMap.builder() .put(40, Level.SEVERE) .put(30, Level.WARNING) .put(20, Level.INFO) @@ -120,11 +124,69 @@ public void updateSettings(ProjectSettingsChangedEvent event) { @Override public

MapNetworkPublisher

create(Set keys) { - // Keep track of ever publisher created. - publisherCount.getAndAdd(1); + // Keep track of every publisher created. + count.getAndAdd(1); return new NTPublisher<>(keys); } + @Override + public NetworkReceiver create(String path) { + count.getAndAdd(1); + return new NTReceiver(path); + } + + private static final class NTReceiver extends NetworkReceiver { + + private int entryListenerFunctionUid; + private Object object = false; + private final List> listeners = new LinkedList<>(); + + protected NTReceiver(String path) { + super(path); + addListener(); + + synchronized (NetworkTable.class) { + NetworkTable.initialize(); + } + } + + private void addListener() { + entryListenerFunctionUid = NetworkTablesJNI.addEntryListener(path, + (uid, key, value, flags) -> { + object = value; + listeners.forEach(c -> c.accept(object)); + }, + ITable.NOTIFY_IMMEDIATE + | ITable.NOTIFY_NEW + | ITable.NOTIFY_UPDATE + | ITable.NOTIFY_DELETE + | ITable.NOTIFY_LOCAL); + } + + @Override + public void addListener(Consumer consumer) { + listeners.add(consumer); + } + + @Override + public Object getValue() { + return object; + } + + @Override + public void close() { + NetworkTablesJNI.removeEntryListener(entryListenerFunctionUid); + + synchronized (NetworkTable.class) { + // This receiver is no longer used. + if (NTManager.count.addAndGet(-1) == 0) { + // We are the last resource using NetworkTables so shut it down + NetworkTable.shutdown(); + } + } + } + } + private static final class NTPublisher

extends MapNetworkPublisher

{ private final ImmutableSet keys; private Optional name = Optional.empty(); @@ -184,8 +246,8 @@ public void close() { } synchronized (NetworkTable.class) { // This publisher is no longer used. - if (NTManager.publisherCount.addAndGet(-1) == 0) { - // We are the last publisher so shut it down + if (NTManager.count.addAndGet(-1) == 0) { + // We are the last resource using NetworkTables so shut it down NetworkTable.shutdown(); } } 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 d5e51dd415..695dbb0fbd 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 @@ -193,5 +193,13 @@ public static SocketHint createNumberSocketHint(final String identifier, defaultValue) { return createNumberSocketHintBuilder(identifier, defaultValue).build(); } + + public static SocketHint createStringSocketHint(final String identifier, + String defaultValue) { + return new SocketHint.Builder(String.class) + .identifier(identifier) + .initialValue(defaultValue) + .build(); + } } } diff --git a/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java new file mode 100644 index 0000000000..9742a4392c --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/sources/NetworkTableEntrySource.java @@ -0,0 +1,156 @@ +package edu.wpi.grip.core.sources; + +import edu.wpi.grip.core.Source; +import edu.wpi.grip.core.events.SourceHasPendingUpdateEvent; +import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.operations.network.MapNetworkReceiverFactory; +import edu.wpi.grip.core.operations.network.NetworkReceiver; +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.util.ExceptionWitness; + +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableList; +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.google.inject.name.Named; +import com.thoughtworks.xstream.annotations.XStreamAlias; + +import java.util.List; +import java.util.Properties; + +/** + * Provides a way to get a {@link Types Type} from a NetworkTable that GRIP is connected to. + */ +@XStreamAlias("grip:NetworkTableEntry") +public class NetworkTableEntrySource extends Source { + + private static final String PATH_PROPERTY = "networktable_path"; + private static final String TYPE_PROPERTY = "BOOLEAN"; + + private final EventBus eventBus; + private final OutputSocket output; + private final String path; + private final Types type; + private NetworkReceiver networkReceiver; + + public interface Factory { + NetworkTableEntrySource create(Properties properties); + + NetworkTableEntrySource create(String path, Types type); + } + + public enum Types { + BOOLEAN { + @Override + protected SocketHint createSocketHint() { + return SocketHints.Outputs.createBooleanSocketHint(Types.BOOLEAN.toString(), false); + } + }, + NUMBER { + @Override + protected SocketHint createSocketHint() { + return SocketHints.Outputs.createNumberSocketHint(Types.NUMBER.toString(), 0.0); + } + }, + STRING { + @Override + protected SocketHint createSocketHint() { + return SocketHints.Outputs.createStringSocketHint(Types.STRING.toString(), ""); + } + }; + + Types() { + + } + + protected abstract SocketHint createSocketHint(); + + @Override + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name()); + } + + public String toProperty() { + return toString().toUpperCase(); + } + } + + @AssistedInject + NetworkTableEntrySource( + EventBus eventBus, + ExceptionWitness.Factory exceptionWitnessFactory, + OutputSocket.Factory osf, + @Named("ntManager") MapNetworkReceiverFactory networkReceiverFactory, + @Assisted Properties properties) { + this(eventBus, + exceptionWitnessFactory, + osf, + networkReceiverFactory, + properties.getProperty(PATH_PROPERTY), + Types.valueOf(properties.getProperty(TYPE_PROPERTY))); + } + + @AssistedInject + NetworkTableEntrySource( + EventBus eventBus, + ExceptionWitness.Factory exceptionWitnessFactory, + OutputSocket.Factory osf, + @Named("ntManager") MapNetworkReceiverFactory networkReceiverFactory, + @Assisted String path, + @Assisted Types type) { + super(exceptionWitnessFactory); + this.eventBus = eventBus; + this.path = path; + this.type = type; + networkReceiver = networkReceiverFactory.create(path); + output = osf.create(type.createSocketHint()); + } + + @Override + public String getName() { + return path; + } + + @Override + protected List createOutputSockets() { + return ImmutableList.of( + output + ); + } + + @Override + protected boolean updateOutputSockets() { + try { + output.setValue(networkReceiver.getValue()); + } catch (ClassCastException ex) { + getExceptionWitness().flagException(ex, getName() + " is not of type " + + output.getSocketHint().getTypeLabel()); + return false; + } + return true; + } + + @Override + public Properties getProperties() { + Properties properties = new Properties(); + properties.setProperty(PATH_PROPERTY, path); + properties.setProperty(TYPE_PROPERTY, type.toProperty()); + return properties; + } + + @Override + public void initialize() { + networkReceiver.addListener(o -> eventBus.post(new SourceHasPendingUpdateEvent(this))); + } + + @Subscribe + public void onSourceRemovedEvent(SourceRemovedEvent event) { + if (event.getSource() == this) { + networkReceiver.close(); + } + } +} diff --git a/core/src/test/java/edu/wpi/grip/core/PipelineTest.java b/core/src/test/java/edu/wpi/grip/core/PipelineTest.java index e7f5445b55..b876322915 100644 --- a/core/src/test/java/edu/wpi/grip/core/PipelineTest.java +++ b/core/src/test/java/edu/wpi/grip/core/PipelineTest.java @@ -4,6 +4,7 @@ import edu.wpi.grip.core.events.ConnectionRemovedEvent; import edu.wpi.grip.core.events.SourceAddedEvent; import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.MockInputSocket; import edu.wpi.grip.core.sockets.MockOutputSocket; @@ -15,6 +16,7 @@ import com.google.common.eventbus.EventBus; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.util.Modules; import org.junit.After; import org.junit.Before; @@ -43,7 +45,8 @@ public class PipelineTest { public void setUp() { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(testModule); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); stepFactory = injector.getInstance(Step.Factory.class); eventBus = injector.getInstance(EventBus.class); pipeline = injector.getInstance(Pipeline.class); diff --git a/core/src/test/java/edu/wpi/grip/core/PythonTest.java b/core/src/test/java/edu/wpi/grip/core/PythonTest.java index ed37e68a5a..cbd4110511 100644 --- a/core/src/test/java/edu/wpi/grip/core/PythonTest.java +++ b/core/src/test/java/edu/wpi/grip/core/PythonTest.java @@ -1,6 +1,7 @@ package edu.wpi.grip.core; import edu.wpi.grip.core.operations.PythonScriptFile; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.Socket; @@ -10,6 +11,7 @@ import com.google.common.eventbus.EventBus; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.util.Modules; import org.junit.After; import org.junit.Before; @@ -31,7 +33,8 @@ public class PythonTest { public void setUp() { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(testModule); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); eventBus = injector.getInstance(EventBus.class); isf = injector.getInstance(InputSocket.Factory.class); osf = injector.getInstance(OutputSocket.Factory.class); diff --git a/core/src/test/java/edu/wpi/grip/core/StepTest.java b/core/src/test/java/edu/wpi/grip/core/StepTest.java index da900f198d..99e7d963df 100644 --- a/core/src/test/java/edu/wpi/grip/core/StepTest.java +++ b/core/src/test/java/edu/wpi/grip/core/StepTest.java @@ -1,5 +1,6 @@ package edu.wpi.grip.core; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.Socket; @@ -9,6 +10,7 @@ import com.google.common.eventbus.EventBus; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.util.Modules; import org.junit.After; import org.junit.Before; @@ -24,7 +26,8 @@ public class StepTest { @Before public void setUp() { testModule.setUp(); - Injector injector = Guice.createInjector(testModule); + Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); InputSocket.Factory isf = injector.getInstance(InputSocket.Factory.class); OutputSocket.Factory osf = injector.getInstance(OutputSocket.Factory.class); additionMeta = new OperationMetaData(AdditionOperation.DESCRIPTION, () -> new diff --git a/core/src/test/java/edu/wpi/grip/core/operations/network/MockGripNetworkModule.java b/core/src/test/java/edu/wpi/grip/core/operations/network/MockGripNetworkModule.java index d3017c439e..1aac03e77c 100644 --- a/core/src/test/java/edu/wpi/grip/core/operations/network/MockGripNetworkModule.java +++ b/core/src/test/java/edu/wpi/grip/core/operations/network/MockGripNetworkModule.java @@ -21,5 +21,9 @@ protected void configure() { bind(ROSNetworkPublisherFactory.class) .annotatedWith(Names.named("rosManager")) .to(MockROSManager.class); + + bind(MapNetworkReceiverFactory.class) + .annotatedWith(Names.named("ntManager")) + .to(MockNetworkReceiver.class); } } diff --git a/core/src/test/java/edu/wpi/grip/core/operations/network/MockNetworkReceiver.java b/core/src/test/java/edu/wpi/grip/core/operations/network/MockNetworkReceiver.java new file mode 100644 index 0000000000..6d90778633 --- /dev/null +++ b/core/src/test/java/edu/wpi/grip/core/operations/network/MockNetworkReceiver.java @@ -0,0 +1,9 @@ +package edu.wpi.grip.core.operations.network; + +public class MockNetworkReceiver implements MapNetworkReceiverFactory { + + @Override + public NetworkReceiver create(String path) { + return null; + } +} diff --git a/core/src/test/java/edu/wpi/grip/core/operations/network/NetworkPackageSanityTest.java b/core/src/test/java/edu/wpi/grip/core/operations/network/NetworkPackageSanityTest.java index 1b15593893..a717ae7e4a 100644 --- a/core/src/test/java/edu/wpi/grip/core/operations/network/NetworkPackageSanityTest.java +++ b/core/src/test/java/edu/wpi/grip/core/operations/network/NetworkPackageSanityTest.java @@ -8,5 +8,6 @@ public class NetworkPackageSanityTest extends AbstractPackageSanityTests { public NetworkPackageSanityTest() { super(); publicApiOnly(); + ignoreClasses(c -> c.getName().contains("Mock")); } } diff --git a/core/src/test/java/edu/wpi/grip/core/operations/network/networktables/TestingNTManager.java b/core/src/test/java/edu/wpi/grip/core/operations/network/networktables/TestingNTManager.java new file mode 100644 index 0000000000..0afd6aa860 --- /dev/null +++ b/core/src/test/java/edu/wpi/grip/core/operations/network/networktables/TestingNTManager.java @@ -0,0 +1,41 @@ +package edu.wpi.grip.core.operations.network.networktables; + +import com.google.inject.Singleton; + +import edu.wpi.first.wpilibj.networktables.NetworkTable; +import edu.wpi.first.wpilibj.networktables.NetworkTablesJNI; + +import java.io.File; +import java.util.logging.Logger; + +/** + * This class encapsulates the way we map various settings to the global NetworkTables state. + */ +@Singleton +public class TestingNTManager extends NTManager implements AutoCloseable { + + private static final Logger logger = Logger.getLogger(TestingNTManager.class.getName()); + + public TestingNTManager() { + // We may have another instance of this method lying around + NetworkTable.shutdown(); + + // Redirect NetworkTables log messages to our own log files. This gets rid of console spam, + // and it also lets + // us grep through NetworkTables messages just like any other messages. + NetworkTablesJNI.setLogger((level, file, line, msg) -> { + String filename = new File(file).getName(); + logger.log(ntLogLevels.get(level), String.format("NetworkTables: %s:%d %s", filename, line, + msg)); + }, 0); + + NetworkTable.setServerMode(); + NetworkTable.initialize(); + } + + @Override + public void close() { + NetworkTable.shutdown(); + } + +} diff --git a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java index 013cd2f0eb..b119685574 100644 --- a/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java +++ b/core/src/test/java/edu/wpi/grip/core/serialization/ProjectTest.java @@ -13,6 +13,7 @@ import edu.wpi.grip.core.events.ProjectSettingsChangedEvent; import edu.wpi.grip.core.events.SourceAddedEvent; import edu.wpi.grip.core.operations.PythonScriptFile; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.settings.ProjectSettings; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; @@ -25,6 +26,7 @@ import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.TypeLiteral; +import com.google.inject.util.Modules; import org.junit.After; import org.junit.Before; @@ -64,7 +66,8 @@ public class ProjectTest { public void setUp() throws Exception { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(testModule); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); connectionFactory = injector .getInstance(Key.get(new TypeLiteral>() { })); diff --git a/core/src/test/java/edu/wpi/grip/core/sources/CameraSourceTest.java b/core/src/test/java/edu/wpi/grip/core/sources/CameraSourceTest.java index 1c1fc1f151..49eac348c8 100644 --- a/core/src/test/java/edu/wpi/grip/core/sources/CameraSourceTest.java +++ b/core/src/test/java/edu/wpi/grip/core/sources/CameraSourceTest.java @@ -2,6 +2,7 @@ import edu.wpi.grip.core.events.UnexpectedThrowableEvent; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.util.ImageLoadingUtility; import edu.wpi.grip.core.util.MockExceptionWitness; @@ -14,6 +15,7 @@ import com.google.common.util.concurrent.Service; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.util.Modules; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -61,7 +63,8 @@ public class CameraSourceTest { public void setUp() throws Exception { this.testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(testModule); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); this.cameraSourceFactory = injector.getInstance(CameraSource.Factory.class); this.osf = injector.getInstance(OutputSocket.Factory.class); diff --git a/core/src/test/java/edu/wpi/grip/core/sources/NetworkTableEntrySourceTest.java b/core/src/test/java/edu/wpi/grip/core/sources/NetworkTableEntrySourceTest.java new file mode 100644 index 0000000000..e8d8eef4b3 --- /dev/null +++ b/core/src/test/java/edu/wpi/grip/core/sources/NetworkTableEntrySourceTest.java @@ -0,0 +1,168 @@ +package edu.wpi.grip.core.sources; + +import edu.wpi.grip.core.events.SourceRemovedEvent; +import edu.wpi.grip.core.operations.network.networktables.TestingNTManager; +import edu.wpi.grip.core.sockets.MockOutputSocketFactory; +import edu.wpi.grip.core.util.MockExceptionWitness; + +import com.google.common.eventbus.EventBus; + +import edu.wpi.first.wpilibj.networktables.NetworkTablesJNI; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NetworkTableEntrySourceTest { + + private final EventBus eventBus; + private final MockOutputSocketFactory osf; + + private NetworkTableEntrySource source; + private TestingNTManager testingNtManager; + + private static final double TEST_NUMBER = 13.13; + private static final String TEST_STRING = "Some test string"; + private static final String BOOLEAN_PATH = "/GRIP/test/boolean"; + private static final String NUMBER_PATH = "/GRIP/test/number"; + private static final String STRING_PATH = "/GRIP/test/string"; + + public NetworkTableEntrySourceTest() { + eventBus = new EventBus(); + osf = new MockOutputSocketFactory(eventBus); + } + + @Before + public void setUp() { + testingNtManager = new TestingNTManager(); + + NetworkTablesJNI.putBoolean(BOOLEAN_PATH, true); + NetworkTablesJNI.putDouble(NUMBER_PATH, TEST_NUMBER); + NetworkTablesJNI.putString(STRING_PATH, TEST_STRING); + } + + @After + public void tearDown() { + eventBus.post(new SourceRemovedEvent(source)); + testingNtManager.close(); + } + + @Test + public void testBoolean() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + BOOLEAN_PATH, + NetworkTableEntrySource.Types.BOOLEAN); + + assertTrue("Socket could not be updated", source.updateOutputSockets()); + assertTrue("The socket's value was false, expected true.", + (boolean) source.getOutputSockets().get(0).getValue().get()); + } + + @Test + public void testBooleanWrongTypeNumber() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + NUMBER_PATH, + NetworkTableEntrySource.Types.BOOLEAN); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + + @Test + public void testBooleanWrongTypeString() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + STRING_PATH, + NetworkTableEntrySource.Types.BOOLEAN); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + + @Test + public void testNumber() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + NUMBER_PATH, + NetworkTableEntrySource.Types.NUMBER); + + assertTrue("Socket could not be updated", source.updateOutputSockets()); + assertEquals("Expected numbers to be equal -- they are not equal", + TEST_NUMBER, (double) source.getOutputSockets().get(0).getValue().get(), 0.00001); + } + + @Test + public void testNumberWrongTypeBoolean() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + BOOLEAN_PATH, + NetworkTableEntrySource.Types.NUMBER); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + + @Test + public void testNumberWrongTypeString() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + STRING_PATH, + NetworkTableEntrySource.Types.NUMBER); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + + @Test + public void testString() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + STRING_PATH, + NetworkTableEntrySource.Types.STRING); + + assertTrue("Socket could not be updated", source.updateOutputSockets()); + assertEquals("Expected Strings to be equal -- they are not equal", + TEST_STRING, source.getOutputSockets().get(0).getValue().get()); + } + + @Test + public void testStringWrongTypeBoolean() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + BOOLEAN_PATH, + NetworkTableEntrySource.Types.STRING); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + + @Test + public void testStringWrongTypeNumber() { + source = new NetworkTableEntrySource(eventBus, + origin -> new MockExceptionWitness(eventBus, origin), + osf, + testingNtManager, + NUMBER_PATH, + NetworkTableEntrySource.Types.STRING); + + assertFalse("The socket was able to update with an invalid type", source.updateOutputSockets()); + } + +} diff --git a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceButton.java b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceButton.java index fb02d993d4..17b1f2741a 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceButton.java +++ b/ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceButton.java @@ -7,6 +7,7 @@ import edu.wpi.grip.core.sources.HttpSource; import edu.wpi.grip.core.sources.ImageFileSource; import edu.wpi.grip.core.sources.MultiImageFileSource; +import edu.wpi.grip.core.sources.NetworkTableEntrySource; import edu.wpi.grip.ui.util.DPIUtility; import edu.wpi.grip.ui.util.SupplierWithIO; @@ -25,13 +26,15 @@ import java.util.function.Consumer; import java.util.function.Predicate; import javafx.application.Platform; +import javafx.beans.value.ChangeListener; import javafx.event.ActionEvent; import javafx.event.EventHandler; +import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; -import javafx.scene.control.Control; +import javafx.scene.control.ComboBox; import javafx.scene.control.Dialog; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; @@ -40,6 +43,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; @@ -59,6 +63,7 @@ public class AddSourceButton extends MenuButton { private final MenuItem webcamButton; private final MenuItem ipcamButton; private final MenuItem httpButton; + private final MenuItem networktablesButton; private Optional

activeDialog = Optional.empty(); @Inject @@ -66,7 +71,8 @@ public class AddSourceButton extends MenuButton { MultiImageFileSource.Factory multiImageSourceFactory, ImageFileSource.Factory imageSourceFactory, CameraSource.Factory cameraSourceFactory, - HttpSource.Factory httpSourceFactory) { + HttpSource.Factory httpSourceFactory, + NetworkTableEntrySource.Factory networkTableSourceFactory) { super("Add Source"); this.eventBus = eventBus; @@ -214,6 +220,46 @@ public class AddSourceButton extends MenuButton { }); }); + networktablesButton = addMenuItem("NetworkTable", + getClass().getResource("/edu/wpi/grip/ui/icons/publish.png"), mouseEvent -> { + final Parent root = this.getScene().getRoot(); + // Show a dialog to pick the server path images will be uploaded on + final String rootString = "/"; + final VBox fields = new VBox(); + final TextField tablePath = new TextField(rootString); + final ComboBox type = new ComboBox<>(); + final SourceDialog dialog = new SourceDialog(root, fields); + + type.setPromptText("Select a data type"); + type.getItems().setAll(NetworkTableEntrySource.Types.values()); + type.setMaxWidth(Double.MAX_VALUE); + fields.getChildren().add(tablePath); + fields.getChildren().add(type); + tablePath.setPromptText("Ex: /GRIP/fps"); + dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(true); // Default disabled + + ChangeListener changeListener = (observable, oldValue, newValue) -> { + boolean valid = tablePath.getText().startsWith(rootString) + && tablePath.getText().length() > rootString.length() + && type.getValue() != null; + dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(!valid); + }; + + tablePath.textProperty().addListener(changeListener); + type.valueProperty().addListener(changeListener); + + dialog.setTitle("Choose NetworkTable path"); + dialog.setHeaderText("Enter the NetworkTable path"); + dialog.showAndWait() + .filter(ButtonType.OK::equals) + .ifPresent(bt -> { + final NetworkTableEntrySource networkTableEntrySource + = networkTableSourceFactory.create(tablePath.getText(), type.getValue()); + networkTableEntrySource.initialize(); + eventBus.post(new SourceAddedEvent(networkTableEntrySource)); + }); + }); + } /** @@ -225,7 +271,6 @@ public class AddSourceButton extends MenuButton { private void loadCamera(Dialog dialog, SupplierWithIO cameraSourceSupplier, Consumer failureCallback) { assert Platform.isFxApplicationThread() : "Should only run in FX thread"; - activeDialog = Optional.of(dialog); dialog.showAndWait().filter(Predicate.isEqual(ButtonType.OK)).ifPresent(result -> { try { // Will try to create the camera with the values from the supplier @@ -237,7 +282,6 @@ private void loadCamera(Dialog dialog, SupplierWithIO loadCamera(dialog, cameraSourceSupplier, failureCallback); } }); - activeDialog = Optional.empty(); } /** @@ -271,6 +315,11 @@ MenuItem getHttpButton() { return httpButton; } + @VisibleForTesting + MenuItem getNetworktablesButton() { + return networktablesButton; + } + @VisibleForTesting void closeDialogs() { activeDialog.ifPresent(dialog -> { @@ -288,21 +337,24 @@ public interface Factory { AddSourceButton create(); } - private static class SourceDialog extends Dialog { + private class SourceDialog extends Dialog { private final Text errorText = new Text(); - private SourceDialog(final Parent root, Control inputField) { + private SourceDialog(final Parent root, Node customField) { super(); + setOnShowing(event -> activeDialog = Optional.of(this)); + setOnHidden(event -> activeDialog = Optional.empty()); + this.getDialogPane().getStyleClass().add(SOURCE_DIALOG_STYLE_CLASS); final GridPane gridContent = new GridPane(); gridContent.setMaxWidth(Double.MAX_VALUE); - GridPane.setHgrow(inputField, Priority.ALWAYS); + GridPane.setHgrow(customField, Priority.ALWAYS); GridPane.setHgrow(errorText, Priority.NEVER); - errorText.wrappingWidthProperty().bind(inputField.widthProperty()); + errorText.setWrappingWidth(customField.getLayoutBounds().getWidth()); gridContent.add(errorText, 0, 0); - gridContent.add(inputField, 0, 1); + gridContent.add(customField, 0, 1); getDialogPane().setContent(gridContent); getDialogPane().setStyle(root.getStyle()); diff --git a/ui/src/test/java/edu/wpi/grip/ui/PaletteTest.java b/ui/src/test/java/edu/wpi/grip/ui/PaletteTest.java index 4581a9bac5..0d16758a77 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/PaletteTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/PaletteTest.java @@ -6,6 +6,7 @@ import edu.wpi.grip.core.Step; import edu.wpi.grip.core.events.OperationAddedEvent; import edu.wpi.grip.core.events.StepAddedEvent; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.util.GripCoreTestModule; @@ -41,7 +42,8 @@ public class PaletteTest extends ApplicationTest { public void start(Stage stage) throws IOException { testModule.setUp(); - Injector injector = Guice.createInjector(Modules.override(testModule).with(new GripUiModule())); + Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); eventBus = injector.getInstance(EventBus.class); FXMLLoader loader = new FXMLLoader(getClass().getResource("Palette.fxml")); diff --git a/ui/src/test/java/edu/wpi/grip/ui/pipeline/AddSourceButtonTest.java b/ui/src/test/java/edu/wpi/grip/ui/pipeline/AddSourceButtonTest.java index 9baac12eab..82a8316fbe 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/pipeline/AddSourceButtonTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/pipeline/AddSourceButtonTest.java @@ -1,6 +1,5 @@ package edu.wpi.grip.ui.pipeline; - import edu.wpi.grip.core.sources.CameraSource; import edu.wpi.grip.core.sources.MockCameraSource; @@ -42,7 +41,9 @@ public void start(Stage stage) { this.eventBus = new EventBus("Test Event Bus"); this.mockCameraSourceFactory = new MockCameraSourceFactory(eventBus); - addSourceView = new AddSourceButton(eventBus, null, null, mockCameraSourceFactory, null); + + addSourceView + = new AddSourceButton(eventBus, null, null, mockCameraSourceFactory, null, null); final Scene scene = new Scene(addSourceView, 800, 600); stage.setScene(scene); @@ -71,6 +72,22 @@ public void testClickOnCreateIPCameraOpensDialog() throws Exception { verifyThat("." + AddSourceButton.SOURCE_DIALOG_STYLE_CLASS, NodeMatchers.isVisible()); } + @Test + @SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") + public void testClickOnCreateHttpOpensDialog() throws Exception { + Platform.runLater(() -> addSourceView.getHttpButton().fire()); + WaitForAsyncUtils.waitForFxEvents(); + verifyThat("." + AddSourceButton.SOURCE_DIALOG_STYLE_CLASS, NodeMatchers.isVisible()); + } + + @Test + @SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert") + public void testClickOnCreateNetworkTableOpensDialog() throws Exception { + Platform.runLater(() -> addSourceView.getNetworktablesButton().fire()); + WaitForAsyncUtils.waitForFxEvents(); + verifyThat("." + AddSourceButton.SOURCE_DIALOG_STYLE_CLASS, NodeMatchers.isVisible()); + } + @Test public void testCreatesSourceStarted() throws Exception { // When @@ -131,7 +148,8 @@ public void start(Stage stage) { this.eventBus = new EventBus("Test Event Bus"); this.mockCameraSourceFactory = new MockCameraSourceFactory(eventBus); - addSourceView = new AddSourceButton(eventBus, null, null, mockCameraSourceFactory, null); + addSourceView + = new AddSourceButton(eventBus, null, null, mockCameraSourceFactory, null, null); final Scene scene = new Scene(addSourceView, 800, 600); stage.setScene(scene); diff --git a/ui/src/test/java/edu/wpi/grip/ui/pipeline/PipelineUITest.java b/ui/src/test/java/edu/wpi/grip/ui/pipeline/PipelineUITest.java index a898481b8a..1516bbc6c8 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/pipeline/PipelineUITest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/pipeline/PipelineUITest.java @@ -9,6 +9,7 @@ import edu.wpi.grip.core.SubtractionOperation; import edu.wpi.grip.core.operations.composite.BlurOperation; import edu.wpi.grip.core.operations.composite.DesaturateOperation; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.util.MockExceptionWitness; @@ -61,8 +62,8 @@ public class PipelineUITest extends ApplicationTest { public void start(Stage stage) { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(Modules.override(testModule).with(new - GripUiModule())); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); eventBus = injector.getInstance(EventBus.class); pipeline = injector.getInstance(Pipeline.class); InputSocket.Factory isf = injector.getInstance(InputSocket.Factory.class); diff --git a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java index 2640c50333..632227c5d3 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/pipeline/input/InputSocketControllerFactoryTest.java @@ -4,6 +4,7 @@ import edu.wpi.grip.core.Palette; import edu.wpi.grip.core.Step; import edu.wpi.grip.core.operations.OperationsFactory; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.InputSocket; import edu.wpi.grip.ui.GripUiModule; import edu.wpi.grip.util.GripCoreTestModule; @@ -56,7 +57,8 @@ public static Collection data() { GripCoreTestModule testModule = new GripCoreTestModule(); testModule.setUp(); - Injector injector = Guice.createInjector(testModule); + Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new MockGripNetworkModule())); final Palette palette = injector.getInstance(Palette.class); final EventBus eventBus = injector.getInstance(EventBus.class); OperationsFactory.create(eventBus).addOperations(); @@ -80,7 +82,8 @@ public static Collection data() { public void start(Stage stage) { testModule = new GripCoreTestModule(); testModule.setUp(); - Injector injector = Guice.createInjector(Modules.override(testModule).with(new GripUiModule())); + Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); inputSocketControllerFactory = injector.getInstance(InputSocketControllerFactory.class); stepFactory = injector.getInstance(Step.Factory.class); gridPane = new GridPane(); diff --git a/ui/src/test/java/edu/wpi/grip/ui/preview/ImageSocketPreviewViewTest.java b/ui/src/test/java/edu/wpi/grip/ui/preview/ImageSocketPreviewViewTest.java index 7ec34290ea..e866ddf8e8 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/preview/ImageSocketPreviewViewTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/preview/ImageSocketPreviewViewTest.java @@ -1,5 +1,6 @@ package edu.wpi.grip.ui.preview; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.SocketHint; import edu.wpi.grip.ui.GripUiModule; @@ -33,8 +34,8 @@ public void start(Stage stage) { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(Modules.override(testModule).with(new - GripUiModule())); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); final ImageSocketPreviewView imageSocketPreviewView = new ImageSocketPreviewView(new MockGripPlatform(new EventBus()), injector.getInstance(OutputSocket.Factory.class) diff --git a/ui/src/test/java/edu/wpi/grip/ui/preview/PointSizeSocketPreviewViewTest.java b/ui/src/test/java/edu/wpi/grip/ui/preview/PointSizeSocketPreviewViewTest.java index cf9afdc075..4cf60c20b1 100644 --- a/ui/src/test/java/edu/wpi/grip/ui/preview/PointSizeSocketPreviewViewTest.java +++ b/ui/src/test/java/edu/wpi/grip/ui/preview/PointSizeSocketPreviewViewTest.java @@ -1,5 +1,6 @@ package edu.wpi.grip.ui.preview; +import edu.wpi.grip.core.operations.network.MockGripNetworkModule; import edu.wpi.grip.core.sockets.OutputSocket; import edu.wpi.grip.core.sockets.SocketHint; import edu.wpi.grip.ui.GripUiModule; @@ -39,8 +40,8 @@ public static class PointSocketPreviewViewTest extends ApplicationTest { public void start(Stage stage) { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(Modules.override(testModule).with(new - GripUiModule())); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); final OutputSocket pointOutputSocket = injector.getInstance(OutputSocket.Factory.class) .create(new SocketHint.Builder<>(Point.class) @@ -80,8 +81,8 @@ public static class SizeSocketPreviewViewTest extends ApplicationTest { public void start(Stage stage) { testModule = new GripCoreTestModule(); testModule.setUp(); - final Injector injector = Guice.createInjector(Modules.override(testModule).with(new - GripUiModule())); + final Injector injector = Guice.createInjector(Modules.override(testModule) + .with(new GripUiModule(), new MockGripNetworkModule())); final OutputSocket sizeOutputSocket = injector.getInstance(OutputSocket.Factory.class) .create(new SocketHint.Builder<>(Size.class)