From 10eb2f5389592a7879cecdda22b43aed7c53e68c Mon Sep 17 00:00:00 2001 From: GroG Date: Wed, 15 Nov 2023 00:03:43 -0800 Subject: [PATCH] resolved or less controversial updates (#1364) --- pom.xml | 59 +++++- .../framework/interfaces/JsonSender.java | 12 ++ .../framework/interfaces/MessageSender.java | 2 +- .../interfaces/SimpleMessageSender.java | 9 + .../service/FiniteStateMachine.java | 46 ++++- .../org/myrobotlab/service/JMonkeyEngine.java | 9 +- .../java/org/myrobotlab/service/OakD.java | 40 +++- .../org/myrobotlab/service/ProgramAB.java | 2 +- .../java/org/myrobotlab/service/Py4j.java | 62 +++++-- .../java/org/myrobotlab/service/Vertx.java | 174 ++++++++++++++++++ .../java/org/myrobotlab/service/WebXR.java | 163 ++++++++++++++++ .../myrobotlab/service/config/OakDConfig.java | 32 ++++ .../service/config/VertxConfig.java | 9 + .../service/config/WebXRConfig.java | 37 ++++ .../org/myrobotlab/service/data/Event.java | 38 ++++ .../myrobotlab/service/data/Orientation.java | 1 + .../org/myrobotlab/service/data/Pose.java | 22 +++ .../org/myrobotlab/service/data/Position.java | 43 +++++ .../service/interfaces/Executor.java | 4 +- .../myrobotlab/service/meta/WebXRMeta.java | 33 ++++ .../org/myrobotlab/vertx/ApiVerticle.java | 104 +++++++++++ .../myrobotlab/vertx/WebSocketHandler.java | 126 +++++++++++++ src/main/resources/resource/Py4j/Py4j.py | 123 ++++++++++++- .../app/service/js/FiniteStateMachineGui.js | 6 +- .../WebGui/app/service/js/WebXRGui.js | 43 +++++ .../WebGui/app/service/views/WebXRGui.html | 64 +++++++ src/main/resources/resource/WebXR.png | Bin 0 -> 20829 bytes .../org/myrobotlab/service/OpenCVTest.java | 31 ---- .../myrobotlab/service/WebGuiSocketTest.java | 137 ++++++++++++++ 29 files changed, 1355 insertions(+), 76 deletions(-) create mode 100644 src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java create mode 100644 src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java create mode 100644 src/main/java/org/myrobotlab/service/Vertx.java create mode 100644 src/main/java/org/myrobotlab/service/WebXR.java create mode 100644 src/main/java/org/myrobotlab/service/config/OakDConfig.java create mode 100644 src/main/java/org/myrobotlab/service/config/VertxConfig.java create mode 100644 src/main/java/org/myrobotlab/service/config/WebXRConfig.java create mode 100644 src/main/java/org/myrobotlab/service/data/Event.java create mode 100644 src/main/java/org/myrobotlab/service/data/Pose.java create mode 100644 src/main/java/org/myrobotlab/service/data/Position.java create mode 100644 src/main/java/org/myrobotlab/service/meta/WebXRMeta.java create mode 100644 src/main/java/org/myrobotlab/vertx/ApiVerticle.java create mode 100644 src/main/java/org/myrobotlab/vertx/WebSocketHandler.java create mode 100644 src/main/resources/resource/WebGui/app/service/js/WebXRGui.js create mode 100644 src/main/resources/resource/WebGui/app/service/views/WebXRGui.html create mode 100644 src/main/resources/resource/WebXR.png create mode 100644 src/test/java/org/myrobotlab/service/WebGuiSocketTest.java diff --git a/pom.xml b/pom.xml index 6e8fdf4829..e6b9aa346c 100644 --- a/pom.xml +++ b/pom.xml @@ -162,7 +162,7 @@ - + org.boofcv boofcv-all @@ -198,6 +198,12 @@ + + org.nanohttpd + nanohttpd + 2.2.0 + + org.bytedeco @@ -1602,6 +1608,34 @@ + + + io.vertx + vertx-core + 4.3.3 + provided + + + io.netty + * + + + + + io.vertx + vertx-web + 4.3.3 + provided + + + io.netty + * + + + + + + @@ -1645,6 +1679,18 @@ + + au.edu.federation.caliko + caliko + 1.3.8 + + + + au.edu.federation.caliko.visualisation + caliko-visualisation + 1.3.8 + + com.github.sarxos @@ -1733,6 +1779,12 @@ + + dev.onvoid.webrtc + webrtc-java + 0.7.0 + + org.mockito @@ -1740,6 +1792,11 @@ 3.12.4 test + + au.edu.federation.caliko.demo + caliko-demo + 1.3.8 + diff --git a/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java b/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java new file mode 100644 index 0000000000..ef2a7c0056 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java @@ -0,0 +1,12 @@ +package org.myrobotlab.framework.interfaces; + +public interface JsonSender { + + /** + * Send interface which takes a json encoded Message. + * For schema look at org.myrobotlab.framework.Message + * @param jsonEncodedMessage + */ + public void send(String jsonEncodedMessage); + +} diff --git a/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java b/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java index ba0f5f1f19..c05a7b16e2 100644 --- a/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java +++ b/src/main/java/org/myrobotlab/framework/interfaces/MessageSender.java @@ -3,7 +3,7 @@ import org.myrobotlab.framework.Message; import org.myrobotlab.framework.TimeoutException; -public interface MessageSender extends NameProvider { +public interface MessageSender extends NameProvider, SimpleMessageSender { /** * Send invoking messages to remote location to invoke {name} instance's diff --git a/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java b/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java new file mode 100644 index 0000000000..a4d6d54a64 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java @@ -0,0 +1,9 @@ +package org.myrobotlab.framework.interfaces; + +import org.myrobotlab.framework.Message; + +public interface SimpleMessageSender { + + public void send(Message msg); + +} diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index a093f61565..d04436f1e3 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -17,7 +17,6 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.FiniteStateMachineConfig; import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition; -import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import com.github.pnavais.machine.StateMachine; @@ -46,8 +45,14 @@ public class FiniteStateMachine extends Service { protected String lastEvent = null; + @Deprecated /* is this deprecated with ServiceConfig.listeners ? */ protected Set messageListeners = new HashSet<>(); + /** + * state history of fsm + */ + protected List history = new ArrayList<>(); + // TODO - .from("A").to("B").on(Messages.ANY) // TODO - .from("A").to("B").on(Messages.EMPTY) @@ -58,6 +63,21 @@ public class Tuple { public Transition transition; public StateTransition stateTransition; } + + public class StateChange { + public String last; + public String current; + public String event; + public StateChange(String last, String current, String event) { + this.last = last; + this.current = current; + this.event = event; + } + + public String toString() { + return String.format("%s --%s--> %s", last, event, current); + } + } private static Transition toFsmTransition(StateTransition state) { Transition transition = new Transition(); @@ -92,6 +112,13 @@ public String getNext(String key) { public void init() { stateMachine.init(); + State state = stateMachine.getCurrent(); + if (history.size() > 100) { + history.remove(0); + } + if (state != null) { + history.add(state.getName()); + } } private String makeKey(String state0, String msgType, String state1) { @@ -167,7 +194,8 @@ public void fire(String event) { log.info("fired event ({}) -> ({}) moves to ({})", event, last == null ? null : last.getName(), current == null ? null : current.getName()); if (last != null && !last.equals(current)) { - invoke("publishNewState", current.getName()); + invoke("publishStateChange", new StateChange(last.getName(), current.getName(), event)); + history.add(current.getName()); } } catch (Exception e) { log.error("fire threw", e); @@ -209,21 +237,21 @@ public List getTransitions() { } /** - * publishes state if changed here + * Publishes state change (current, last and event) * - * @param state + * @param stateChange * @return */ - public String publishNewState(String state) { - log.error("publishNewState {}", state); + public StateChange publishStateChange(StateChange stateChange) { + log.info("publishStateChange {}", stateChange); for (String listener : messageListeners) { ServiceInterface service = Runtime.getService(listener); if (service != null) { - org.myrobotlab.framework.Message msg = org.myrobotlab.framework.Message.createMessage(getName(), listener, CodecUtils.getCallbackTopicName(state), null); + org.myrobotlab.framework.Message msg = org.myrobotlab.framework.Message.createMessage(getName(), listener, CodecUtils.getCallbackTopicName(stateChange.current), null); service.in(msg); } } - return state; + return stateChange; } @Override @@ -391,7 +419,7 @@ public void setCurrent(String state) { stateMachine.setCurrent(state); current = stateMachine.getCurrent(); if (last != null && !last.equals(current)) { - invoke("publishNewState", current.getName()); + invoke("publishStateChange", new StateChange(last.getName(), current.getName(), null)); } } catch (Exception e) { log.error("setCurrent threw", e); diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index a22156778e..9ca917a6cf 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -7,8 +7,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.FloatBuffer; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -52,7 +50,6 @@ import org.myrobotlab.net.Connection; import org.myrobotlab.sensor.EncoderData; import org.myrobotlab.sensor.EncoderListener; -import org.myrobotlab.service.abstracts.AbstractComputerVision; import org.myrobotlab.service.config.JMonkeyEngineConfig; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.Gateway; @@ -405,10 +402,12 @@ public void attach(Attachable attachable) throws Exception { // this is to support future (non-Java) classes that cannot be instantiated // and // are subclassed in a proxy class with getType() overloaded for to identify + /**
 DO NOT NEED THIS UNTIL JMONKEY DISPLAYS VIDEO DATA - SLAM MAPPING
     if (service.getTypeKey().equals("org.myrobotlab.service.OpenCV")) {
       AbstractComputerVision cv = (AbstractComputerVision) service;
       subscribe(service.getName(), "publishCvData");
-    }
+    }
+ */ if (service.getTypeKey().equals("org.myrobotlab.service.Servo")) { // non-batched - "instantaneous" move data subscription @@ -1469,7 +1468,7 @@ public void onAnalog(String name, float keyPressed, float tpf) { // PAN -- works(ish) if (mouseMiddle && shiftLeft) { - log.info("PAN !!!!"); + log.debug("panning"); switch (name) { case "mouse-axis-x": case "mouse-axis-x-negative": diff --git a/src/main/java/org/myrobotlab/service/OakD.java b/src/main/java/org/myrobotlab/service/OakD.java index 99e9fdbfea..7c0789211c 100644 --- a/src/main/java/org/myrobotlab/service/OakD.java +++ b/src/main/java/org/myrobotlab/service/OakD.java @@ -1,10 +1,12 @@ package org.myrobotlab.service; import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.Status; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.process.GitHub; +import org.myrobotlab.service.config.OakDConfig; import org.slf4j.Logger; /** * @@ -14,16 +16,50 @@ * @author GroG * */ -public class OakD extends Service { +public class OakD extends Service { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(OakD.class); + private transient Py4j py4j = null; + private transient Git git = null; + public OakD(String n, String id) { super(n, id); } + public void startService() { + super.startService(); + + py4j = (Py4j)startPeer("py4j"); + git = (Git)startPeer("git"); + + if (config.py4jInstall) { + installDepthAi(); + } + + } + + /** + * starting install of depthapi + */ + public void publishInstallStart() { + } + + public Status publishInstallFinish() { + return Status.error("depth ai install was not successful"); + } + + /** + * For depthai we need to clone its repo and install requirements + * + */ + public void installDepthAi() { + + //git.clone("./", config.depthaiCloneUrl) + py4j.exec(""); + } public static void main(String[] args) { try { diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index 29b8653ad8..db06861aaa 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -792,7 +792,7 @@ public String addBotPath(String path) { broadcastState(); } else { - error("invalid bot path - a bot must be a directory with a subdirectory named \"aiml\""); + error("invalid bot path %s - a bot must be a directory with a subdirectory named \"aiml\"", path); return null; } return path; diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java index 8c5e59b502..909270e77c 100644 --- a/src/main/java/org/myrobotlab/service/Py4j.java +++ b/src/main/java/org/myrobotlab/service/Py4j.java @@ -9,7 +9,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.bytedeco.javacpp.Loader; import org.myrobotlab.codec.CodecUtils; @@ -21,9 +20,11 @@ import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.net.Connection; import org.myrobotlab.service.config.Py4jConfig; import org.myrobotlab.service.data.Script; import org.myrobotlab.service.interfaces.Executor; +import org.myrobotlab.service.interfaces.Gateway; import org.slf4j.Logger; import py4j.GatewayServer; @@ -53,7 +54,7 @@ * * @author GroG */ -public class Py4j extends Service implements GatewayServerListener { +public class Py4j extends Service implements GatewayServerListener, Gateway { /** * POJO class to tie all the data elements of a external python process @@ -234,15 +235,6 @@ private String getClientKey(Py4JServerConnection gatewayConnection) { return String.format("%s:%d", gatewayConnection.getSocket().getInetAddress(), gatewayConnection.getSocket().getPort()); } - /** - * return a set of client connections - probably could be deprecated to a - * single client, but was not sure - * - * @return - */ - public Set getClients() { - return clients.keySet(); - } /** * get listing of filesystem files location will be data/Py4j/{serviceName} @@ -336,7 +328,19 @@ public boolean preProcessHook(Message msg) { // TODO - determine clients are connected .. how many clients etc.. try { if (handler != null) { - handler.invoke(msg.method, msg.data); + // afaik - Py4j does some kind of magical encoding to get a JavaObject + // back to the Python process, but: + // 1. its useless for users - no way to access the content ? + // 2. you can't do anything with it + // So, I've chosen to json encode it here, and the Py4j.py MessageHandler will + // decode it into a Python dictionary \o/ + // we do single encoding including the parameter array - there is no header needed + // with method and other details, as the invoke here is invoking directly in the + // Py4j.py script + + String json = CodecUtils.toJson(msg); + // handler.invoke(msg.method, json); + handler.send(json); } else { error("preProcessHook handler is null"); } @@ -588,7 +592,37 @@ public static void main(String[] args) { log.error("main threw", e); } } - - + + @Override + public void connect(String uri) throws Exception { + // host:port of python process running py4j ??? + + } + + /** + * Remote in this context is the remote python process + */ + @Override + public void sendRemote(Message msg) throws Exception { + log.info("sendRemote"); + String jsonMsg = CodecUtils.toJson(msg); + handler.send(jsonMsg); + } + + @Override + public boolean isLocal(Message msg) { + return Runtime.getInstance().isLocal(msg); + } + + @Override + public List getClientIds() { + return Runtime.getInstance().getConnectionUuids(getName()); + } + + @Override + public Map getClients() { + return Runtime.getInstance().getConnections(getName()); + } + } diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java new file mode 100644 index 0000000000..09cd1132ad --- /dev/null +++ b/src/main/java/org/myrobotlab/service/Vertx.java @@ -0,0 +1,174 @@ +package org.myrobotlab.service; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.framework.Message; +import org.myrobotlab.framework.Service; +import org.myrobotlab.logging.Level; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.net.Connection; +import org.myrobotlab.service.config.VertxConfig; +import org.myrobotlab.service.interfaces.Gateway; +import org.myrobotlab.vertx.ApiVerticle; +import org.slf4j.Logger; + +import io.vertx.core.VertxOptions; +import io.vertx.core.http.ServerWebSocket; + +/** + * Vertx gateway - used to support a http and websocket gateway for myrobotlab. + * Write business logic in Verticles. Also, try not to write any logic besides initialization inside start() method. + * + * It currently does not utilize the Vertx event bus - which is pretty much the most important part of Vertx. + * TODO: take advantage of publishing on the event bus + * + * @see https://medium.com/@pvub/https-medium-com-pvub-vert-x-workers-6a8df9b2b9ee + * + * @author GroG + * + */ +public class Vertx extends Service implements Gateway { + + private static final long serialVersionUID = 1L; + + private transient io.vertx.core.Vertx vertx = null; + + public final static Logger log = LoggerFactory.getLogger(Vertx.class); + + public Vertx(String n, String id) { + super(n, id); + } + + /** + * deploys a http and websocket verticle on a secure TLS channel with self signed certificate + */ + public void start() { + log.info("starting driver"); + + /** + * FIXME - might have to revisit this This is a block comment, but takes + * advantage of javadoc pre non-formatting in ide to preserve the code + * formatting + * + *
+     * 
+     * final Vertx that = this;
+     * 
+     * java.lang.Runtime.getRuntime().addShutdownHook(new Thread() {
+     *   public void run() {
+     *     System.out.println("Running Shutdown Hook");
+     *     that.stop();
+     *   }
+     * });
+     * 
+     * 
+ */ + + vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000)); + vertx.deployVerticle(new ApiVerticle(this)); + + } + + @Override + public void startService() { + super.startService(); + start(); + } + + @Override + public void stopService() { + super.stopService(); + stop(); + } + + /** + * Undeploy the verticle serving http and ws + */ + public void stop() { + log.info("stopping driver"); + Set ids = vertx.deploymentIDs(); + for (String id : ids) { + vertx.undeploy(id, (result) -> { + if (result.succeeded()) { + log.info("undeploy succeeded"); + } else { + log.error("undeploy failed"); + } + }); + } + } + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + Vertx vertx = (Vertx) Runtime.start("vertx", "Vertx"); + vertx.start(); + + InMoov2 i01 = (InMoov2)Runtime.start("i01", "InMoov2"); + // i01.startSimulator(); + JMonkeyEngine jme = (JMonkeyEngine)i01.startPeer("simulator"); +// Runtime.start("python", "Python"); +// + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + // webgui.setSsl(true); + webgui.autoStartBrowser(false); + webgui.setPort(8888); + webgui.startService(); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + // FIXME - refactor for bare minimum + + @Override /* FIXME "Gateway" is server/service oriented not connecting thing - remove this */ + public void connect(String uri) throws URISyntaxException { + // TODO Auto-generated method stub + + } + + @Override /* FIXME not much point of these - as they are all consistently using Runtime's centralized connection info */ + public List getClientIds() { + return Runtime.getInstance().getConnectionUuids(getName()); + } + + @Override /* FIXME not much point of these - as they are all consistently using Runtime's centralized connection info */ + public Map getClients() { + return Runtime.getInstance().getConnections(getName()); + } + + + @Override /* FIXME this is the one and probably "only" relevant method for Gateway - perhaps a handle(Connection c) */ + public void sendRemote(Message msg) throws Exception { + log.info("sendRemote {}", msg.toString()); + // FIXME MUST BE DIRECT THREAD FROM BROADCAST NOT OUTBOX !!! + msg.addHop(getId()); + Map clients = getClients(); + for(Connection c: clients.values()) { + try { + ServerWebSocket socket = (ServerWebSocket)c.get("websocket"); + String json = CodecUtils.toJsonMsg(msg); + socket.writeTextMessage(json); + } catch(Exception e) { + error(e); + } + } + // broadcastMode - iterate through clients send all + } + + @Override + public boolean isLocal(Message msg) { + return Runtime.getInstance().isLocal(msg); } + + public io.vertx.core.Vertx getVertx() { + return vertx; + } +} diff --git a/src/main/java/org/myrobotlab/service/WebXR.java b/src/main/java/org/myrobotlab/service/WebXR.java new file mode 100644 index 0000000000..509620832e --- /dev/null +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -0,0 +1,163 @@ +package org.myrobotlab.service; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.framework.Service; +import org.myrobotlab.kinematics.Point; +import org.myrobotlab.logging.Level; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.math.MapperSimple; +import org.myrobotlab.service.config.WebXRConfig; +import org.myrobotlab.service.data.Event; +import org.myrobotlab.service.data.Pose; +import org.slf4j.Logger; + +public class WebXR extends Service { + + private static final long serialVersionUID = 1L; + + public final static Logger log = LoggerFactory.getLogger(WebXR.class); + + public WebXR(String n, String id) { + super(n, id); + } + + public Event publishEvent(Event event) { + if (log.isDebugEnabled()) { + log.debug("publishEvent {}", event); + } + + String path = String.format("event.%s.%s", event.meta.get("handedness"), event.type); + if (event.value != null) { + path = path + "." + event.value.toString(); + } + + if (config.eventMappings.containsKey(path)) { + // TODO - future might be events -> message that goes to ServoMixer .e.g mixer.playGesture("closeHand") + // or sadly Python execute script for total chaos :P + invoke("publishJointAngles", config.eventMappings.get(path)); + } + + return event; + } + + /** + * Pose is the x,y,z and pitch, roll, yaw of all the devices WebXR found. + * Hopefully, this includes headset, and hand controllers. + * WebXRConfig processes a mapping between these values (usually in radians) to + * servo positions, and will then publish JointAngles for servos. + * + * @param pose + * @return + */ + public Pose publishPose(Pose pose) { + if (log.isDebugEnabled()) { + log.debug("publishPose {}", pose); + } + // process mappings config into joint angles + Map map = new HashMap<>(); + + String path = String.format("%s.orientation.roll", pose.name); + if (config.controllerMappings.containsKey(path)) { + Map mapper = config.controllerMappings.get(path); + for (String name : mapper.keySet()) { + map.put(name, mapper.get(name).calcOutput(pose.orientation.roll)); + } + } + + path = String.format("%s.orientation.pitch", pose.name); + if (config.controllerMappings.containsKey(path)) { + Map mapper = config.controllerMappings.get(path); + for (String name : mapper.keySet()) { + map.put(name, mapper.get(name).calcOutput(pose.orientation.pitch)); + } + } + + path = String.format("%s.orientation.yaw", pose.name); + if (config.controllerMappings.containsKey(path)) { + Map mapper = config.controllerMappings.get(path); + for (String name : mapper.keySet()) { + map.put(name, mapper.get(name).calcOutput(pose.orientation.yaw)); + } + } + + InverseKinematics3D ik = (InverseKinematics3D)Runtime.getService("ik3d"); + if (ik != null && pose.name.equals("left")) { + ik.setCurrentArm("left", InMoov2Arm.getDHRobotArm("i01", "left")); + + ik.centerAllJoints("left"); + + for (int i = 0; i < 1000; ++i) { + + ik.centerAllJoints("left"); + ik.moveTo("left", 0, 0.0+ i * 0.02, 0.0); + + + // ik.moveTo(pose.name, new Point(0, -200, 50)); + } + + // map name + // and then map all position and rotation too :P + Point p = new Point(70 + pose.position.x, -550 + pose.position.y, pose.position.z); + + ik.moveTo(pose.name, p); + } + + if (map.size() > 0) { + invoke("publishJointAngles", map); + } + + // TODO - publishQuaternion + // invoke("publishQuaternion", map); + + return pose; + } + + public Map publishJointAngles(Map map) { + for (String name: map.keySet()) { + log.info("{}.moveTo {}", name, map.get(name)); + } + return map; + } + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + // identical to command line start + // Runtime.startConfig("inmoov2"); + + + // normal non-config launch + // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + + + // config launch + Runtime.startConfig("webxr"); + + boolean done = true; + if (done) + return; + + Runtime.startConfig("webxr"); + boolean done2 = true; + if (done2) + return; + + Runtime.start("webxr", "WebXR"); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + // webgui.setSsl(true); + webgui.autoStartBrowser(false); + webgui.startService(); + Runtime.start("vertx", "Vertx"); + InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); + i01.startPeer("simulator"); + + } catch (Exception e) { + log.error("main threw", e); + } + } +} diff --git a/src/main/java/org/myrobotlab/service/config/OakDConfig.java b/src/main/java/org/myrobotlab/service/config/OakDConfig.java new file mode 100644 index 0000000000..6a7295da1c --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/OakDConfig.java @@ -0,0 +1,32 @@ +package org.myrobotlab.service.config; + +import org.myrobotlab.framework.Plan; + +public class OakDConfig extends ServiceConfig { + + + /** + * install through py4j + */ + public boolean py4jInstall = true; + + /** + * the depthai clone + */ + public String depthaiCloneUrl = "https://github.com/luxonis/depthai.git"; + + /** + * pin the repo + */ + public String depthAiSha = "dde0ba57dba673f67a62e4fb080f22d6cfcd3224"; + + @Override + public Plan getDefault(Plan plan, String name) { + super.getDefault(plan, name); + addDefaultPeerConfig(plan, name, "py4j", "Py4j"); + addDefaultPeerConfig(plan, name, "git", "Git"); + return plan; + } + + +} diff --git a/src/main/java/org/myrobotlab/service/config/VertxConfig.java b/src/main/java/org/myrobotlab/service/config/VertxConfig.java new file mode 100644 index 0000000000..f2119d8ddd --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/VertxConfig.java @@ -0,0 +1,9 @@ +package org.myrobotlab.service.config; + +public class VertxConfig extends ServiceConfig { + + public Integer port = 8443; + public Integer workerCount = 1; + public boolean ssl = true; + +} diff --git a/src/main/java/org/myrobotlab/service/config/WebXRConfig.java b/src/main/java/org/myrobotlab/service/config/WebXRConfig.java new file mode 100644 index 0000000000..2990f1c60d --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/WebXRConfig.java @@ -0,0 +1,37 @@ +package org.myrobotlab.service.config; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.framework.Plan; +import org.myrobotlab.math.MapperSimple; + +public class WebXRConfig extends ServiceConfig { + + public Map> eventMappings = new HashMap<>(); + + public Map> controllerMappings = new HashMap<>(); + + + public Plan getDefault(Plan plan, String name) { + super.getDefault(plan, name); + + Map map = new HashMap<>(); + MapperSimple mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.neck", mapper); + controllerMappings.put("head.orientation.pitch", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.rothead", mapper); + controllerMappings.put("head.orientation.yaw", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.roll", mapper); + controllerMappings.put("head.orientation.roll", map); + + return plan; + } + +} diff --git a/src/main/java/org/myrobotlab/service/data/Event.java b/src/main/java/org/myrobotlab/service/data/Event.java new file mode 100644 index 0000000000..9a3572035c --- /dev/null +++ b/src/main/java/org/myrobotlab/service/data/Event.java @@ -0,0 +1,38 @@ +package org.myrobotlab.service.data; + +import java.util.Map; + +/** + * Generalized Event POJO class, started by WebXR but intended to be used + * in other event generating services + * + * @author GroG + * + */ +public class Event { + /** + * Identifier of the event typically its the identifier of some + * detail of the source of the event e.g. in WebXR it is the uuid + */ + public String id; + + /** + * type of event WebXR has sqeezestart sqeezeend and others + */ + public String type; + + + /** + * Value of the event, could be string or numeric or boolean + */ + public Object value; + + + /** + * Meta data regarding the event, could be additional values like + * "handedness" in WebXR + */ + public Map meta; + + +} diff --git a/src/main/java/org/myrobotlab/service/data/Orientation.java b/src/main/java/org/myrobotlab/service/data/Orientation.java index b2d5d5658e..b7df4102dc 100644 --- a/src/main/java/org/myrobotlab/service/data/Orientation.java +++ b/src/main/java/org/myrobotlab/service/data/Orientation.java @@ -11,6 +11,7 @@ public class Orientation { public Double roll = null; public Double pitch = null; public Double yaw = null; + public String src = null; // default constructor (values will be null until set) public Orientation() { diff --git a/src/main/java/org/myrobotlab/service/data/Pose.java b/src/main/java/org/myrobotlab/service/data/Pose.java new file mode 100644 index 0000000000..767d9be81d --- /dev/null +++ b/src/main/java/org/myrobotlab/service/data/Pose.java @@ -0,0 +1,22 @@ +package org.myrobotlab.service.data; + +public class Pose { + public String name = null; + public Long ts = null; + public Position position = null; + public Orientation orientation = null; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("name:%s", name)); + if (position != null) { + sb.append(String.format(" x:%.2f y:%.2f z:%.2f", position.x, position.y, position.z)); + } + if (orientation != null) { + sb.append(String.format(" roll:%.2f pitch:%.2f yaw:%.2f", orientation.roll, orientation.pitch, orientation.yaw)); + } + return sb.toString(); + } + + +} diff --git a/src/main/java/org/myrobotlab/service/data/Position.java b/src/main/java/org/myrobotlab/service/data/Position.java new file mode 100644 index 0000000000..83fe574a44 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/data/Position.java @@ -0,0 +1,43 @@ +package org.myrobotlab.service.data; + +public class Position { + + public Double x; + public Double y; + public Double z; + public String src; + + public Position(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Position(double x, double y) { + this.x = x; + this.y = y; + } + + public Position(int x, int y, int z) { + this.x = (double) x; + this.y = (double) y; + this.z = (double) z; + } + + public Position(int x, int y) { + this.x = (double) x; + this.y = (double) y; + } + + public Position(float x, float y, float z) { + this.x = (double) x; + this.y = (double) y; + this.z = (double) z; + } + + public Position(float x, float y) { + this.x = (double) x; + this.y = (double) y; + } + +} diff --git a/src/main/java/org/myrobotlab/service/interfaces/Executor.java b/src/main/java/org/myrobotlab/service/interfaces/Executor.java index e1566c015a..fc45803c10 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/Executor.java +++ b/src/main/java/org/myrobotlab/service/interfaces/Executor.java @@ -1,6 +1,6 @@ package org.myrobotlab.service.interfaces; -import org.myrobotlab.framework.interfaces.Invoker; +import org.myrobotlab.framework.interfaces.JsonSender; /** * Interface to a Executor - currently only utilized by Py4j to @@ -10,7 +10,7 @@ * @author GroG * */ -public interface Executor extends Invoker { +public interface Executor extends JsonSender { /** * exec in Python - executes arbitrary code diff --git a/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java b/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java new file mode 100644 index 0000000000..36efe701ad --- /dev/null +++ b/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java @@ -0,0 +1,33 @@ +package org.myrobotlab.service.meta; + +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.meta.abstracts.MetaData; +import org.slf4j.Logger; + +public class WebXRMeta extends MetaData { + private static final long serialVersionUID = 1L; + public final static Logger log = LoggerFactory.getLogger(WebXRMeta.class); + + /** + * This class is contains all the meta data details of a service. It's peers, + * dependencies, and all other meta data related to the service. + * + */ + public WebXRMeta() { + + // add a cool description + addDescription("WebXR allows hmi devices to add input and get data back from mrl"); + + // false will prevent it being seen in the ui + setAvailable(true); + + // add it to one or many categories + addCategory("remote","control"); + + // add a sponsor to this service + // the person who will do maintenance + // setSponsor("GroG"); + + } + +} diff --git a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java new file mode 100644 index 0000000000..4dc470f1df --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -0,0 +1,104 @@ +package org.myrobotlab.vertx; + +import org.myrobotlab.service.config.VertxConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.net.SelfSignedCertificate; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.CorsHandler; +import io.vertx.ext.web.handler.StaticHandler; + +/** + * verticle to handle api requests + * + * @author GroG + */ +public class ApiVerticle extends AbstractVerticle { + + public final static Logger log = LoggerFactory.getLogger(ApiVerticle.class); + + private Router router; + + transient private org.myrobotlab.service.Vertx service; + + public ApiVerticle(org.myrobotlab.service.Vertx service) { + super(); + this.service = service; + } + + @Override + public void start() throws Exception { + // process configuration and create handlers + log.info("starting api verticle"); + VertxConfig config = (VertxConfig) service.getConfig(); + + // create a router + router = Router.router(vertx); + + // handle cors requests + router.route().handler(CorsHandler.create("*").allowedMethod(HttpMethod.GET).allowedMethod(HttpMethod.OPTIONS).allowedHeader("Accept").allowedHeader("Authorization") + .allowedHeader("Content-Type")); + + // static file routing - this is from a npm "build" ... but durin gdevelop its far + // easier to use setupProxy.js from a npm start .. but deployment would be easier with a "build" + + // new UI + // StaticHandler root = StaticHandler.create("../robotlab-x-app/build/"); + + // old UI (runtime vs dev time...) + StaticHandler root = StaticHandler.create("src/main/resources/resource/WebGui/app/"); + root.setCachingEnabled(false); + root.setDirectoryListing(true); + root.setIndexPage("index.html"); + // root.setAllowRootFileSystemAccess(true); + // root.setWebRoot(null); + + // VideoStreamHandler video = new VideoStreamHandler(service); + + // router.route("/video/*").handler(video); + router.route("/*").handler(root); + + + // router.get("/health").handler(this::generateHealth); + // router.get("/api/transaction/:customer/:tid").handler(this::handleTransaction); + + // create the HTTP server and pass the + // "accept" method to the request handler + HttpServerOptions httpOptions = new HttpServerOptions(); + + if (config.ssl) { + SelfSignedCertificate certificate = SelfSignedCertificate.create(); + httpOptions.setSsl(true); + httpOptions.setKeyCertOptions(certificate.keyCertOptions()); + httpOptions.setTrustOptions(certificate.trustOptions()); + } + httpOptions.setPort(config.port); + + + HttpServer server = vertx.createHttpServer(httpOptions); + // TODO - this is where multiple workers would be defined + // .createHttpServer() + + // WebSocketHandler webSocketHandler = new WebSocketHandler(service); + // server.webSocketHandler(webSocketHandler); + + // FIXME - don't do "long" or "common" processing in the start() + // FIXME - how to do this -> server.webSocketHandler(this::handleWebSocket); + server.webSocketHandler(new WebSocketHandler(service)); + server.requestHandler(router); + // start servers + server.listen(); + } + + + @Override + public void stop() throws Exception { + log.info("stopping api verticle"); + } + +} diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java new file mode 100644 index 0000000000..69b56957f3 --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java @@ -0,0 +1,126 @@ +package org.myrobotlab.vertx; + +import java.lang.reflect.Method; + +import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.framework.MethodCache; +import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.net.Connection; +import org.myrobotlab.service.Runtime; +import org.slf4j.Logger; + +import io.vertx.core.Handler; +import io.vertx.core.http.ServerWebSocket; + +/** + * Minimal Handler for all websocket messages coming from the react js client. + * + * TODO - what else besides text messages - websocket binary streams ??? text + * stream ? + * + * @author GroG + * + */ +public class WebSocketHandler implements Handler { + + public final static Logger log = LoggerFactory.getLogger(WebSocketHandler.class); + + /** + * reference to the MRL Vertx service / websocket and http server + */ + transient private org.myrobotlab.service.Vertx service = null; + + /** + * reference to the websocket text message handler + */ + TextMessageHandler textMessageHandler = null; + + public static class TextMessageHandler implements Handler { + + org.myrobotlab.service.Vertx service = null; + + public TextMessageHandler(org.myrobotlab.service.Vertx service) { + this.service = service; + } + + @Override + public void handle(String json) { + log.info("handling {}", json); + + Method method; + try { + + org.myrobotlab.framework.Message msg = CodecUtils.fromJson(json, org.myrobotlab.framework.Message.class); + + Class clazz = Runtime.getClass(msg.name); + if (clazz == null) { + log.error("cannot derive local type from service {}", msg.name); + return; + } + + MethodCache cache = MethodCache.getInstance(); + Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, msg.data); + + method = cache.getMethod(clazz, msg.method, params); + if (method == null) { + service.error("method cache could not find %s.%s(%s)", clazz.getSimpleName(), msg.method, msg.data); + return; + } + + // FIXME - probably shouldn't be invoking, probable should be putting + // the message on the out queue ... not sure + ServiceInterface si = Runtime.getService(msg.name); + // Object ret = method.invoke(si, params); + + // put msg on mrl msg bus :) + // service.in(msg); <- NOT DECODE PARAMS !! + + // if ((new Random()).nextInt(100) == 0) { + // ctx.close(); - will close the websocket !!! + // } else { + // ctx.writeTextMessage("ping"); Useful is writing back + // } + + // replace with typed parameters + msg.data = params; + // queue the message + si.in(msg); + + } catch (Exception e) { + service.error(e); + } + } + } + + public WebSocketHandler(org.myrobotlab.service.Vertx service) { + this.service = service; + this.textMessageHandler = new TextMessageHandler(service); + } + + @Override + public void handle(ServerWebSocket socket) { + // FIXME - get "id" from js client - need something unique from the js + // client + // String id = r.getRequest().getParameter("id"); + String id = String.format("vertx-%s", service.getName()); + // String uuid = UUID.randomUUID().toString(); + String uuid = socket.binaryHandlerID(); + Connection connection = new Connection(uuid, id, service.getName()); + connection.put("c-type", service.getSimpleName()); + connection.put("gateway", service.getName()); + connection.putTransient("websocket", socket); + Runtime.getInstance().addConnection(uuid, id, connection); + // ctx.writeTextMessage("ping"); FIXME - query ? + // FIXME - thread-safe ? how many connections mapped to objects ? + socket.textMessageHandler(textMessageHandler); + log.info("new ws connection {}", uuid); + + socket.closeHandler(close -> { + log.info("closing {}", socket.binaryHandlerID()); + Runtime.getInstance().removeConnection(socket.binaryHandlerID()); + }); + + } + +} diff --git a/src/main/resources/resource/Py4j/Py4j.py b/src/main/resources/resource/Py4j/Py4j.py index 0cdb5b3bce..3c75cd94c5 100644 --- a/src/main/resources/resource/Py4j/Py4j.py +++ b/src/main/resources/resource/Py4j/Py4j.py @@ -13,13 +13,78 @@ # the gateway import sys - +import json +from abc import ABC, abstractmethod from py4j.java_collections import JavaObject, JavaClass from py4j.java_gateway import JavaGateway, CallbackServerParameters, GatewayParameters -runtime = None +class Service(ABC): + def __init__(self, name): + self.java_object = runtime.start(name, self.getType()) + + def __getattr__(self, attr): + # Delegate attribute access to the underlying Java object + return getattr(self.java_object, attr) + + def __str__(self): + # Delegate string representation to the underlying Java object + return str(self.java_object) + + def subscribe(self, event): + print("subscribe") + self.java_object.subscribe(event) + + @abstractmethod + def getType(self): + pass + + +class NeoPixel(Service): + def __init__(self, name): + super().__init__(name) + + def getType(self): + return "NeoPixel" + + def onFlash(self): + print("onFlash") + + +class InMoov2(Service): + def __init__(self, name): + super().__init__(name) + self.subscribe('onStateChange') + + def getType(self): + return "InMoov2" + + def onOnStateChange(self, state): + print("onOnStateChange") + print(state) + print(state.get('last')) + print(state.get('current')) + print(state.get('event')) + + +# TODO dynamically add classes that you don't bother to check in +# class Runtime(Service): +# def __init__(self, name): +# super().__init__(name) + + +# FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!! +runtime = None + +# TODO - rename to mrl_lib ? +# e.g. +# mrl = mrl_lib.connect("localhost", 1099) +# i01 = InMoov("i01", mrl) +# or +# runtime = mrl_lib.connect("localhost", 1099) # JVM connection Py4j instance needed for a gateway +# runtime.start("i01", "InMoov2") # starts Java service +# runtime.start("nativePythonService", "NativePythonClass") # starts Python service no gateway needed class MessageHandler(object): """ The class responsible for receiving and processing Py4j messages, @@ -41,10 +106,34 @@ def __init__(self): python_server_entry_point=self, gateway_parameters=GatewayParameters(auto_convert=True)) self.runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance() + # FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!! runtime = self.runtime self.py4j = None # need to wait until name is set print("initialized ... waiting for name to be set") + def construct_runtime(self): + """ + Constructs a new Runtime instance and returns it. + """ + jvm_runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance() + + # Define class attributes and methods as dictionaries + class_attributes = { + 'x': 0, + 'y': 0, + 'move': lambda self, dx, dy: setattr(self, 'x', self.x + dx) or setattr(self, 'y', self.y + dy), + 'get_position': lambda self: (self.x, self.y), + } + + # Create the class dynamically using the type() function + MyDynamicClass = type('MyDynamicClass', (object,), class_attributes) + + # Create an instance of the dynamically created class + obj = MyDynamicClass() + + + return self.runtime + # Define the callback function def handle_connection_break(self): # Add your custom logic here to handle the connection break @@ -78,11 +167,15 @@ def setName(self, name): print("reference to runtime") # TODO print env vars PYTHONPATH etc return name + + def getRuntime(self): + return self.runtime def exec(self, code): """ Executes Python code in the global namespace. - All exceptions are caught and printed so that the Python subprocess doesn't crash. + All exceptions are caught and printed so that the + Python subprocess doesn't crash. :param code: The Python code to execute. :type code: str @@ -94,22 +187,38 @@ def exec(self, code): except Exception as e: print(e) - def invoke(self, method, data=()): + def send(self, json_msg): + msg = json.loads(json_msg) + if msg.get("data") is None or msg.get("data") == []: + globals()[msg.get("method")]() + else: + globals()[msg.get("method")](*msg.get("data")) + + # equivalent to JS onMessage + def invoke(self, method, data=None): """ Invoke a function from the global namespace with the given parameters. :param method: The name of the function to invoke. :type method: str - :param data: The parameters to pass to the function, defaulting to no parameters. + :param data: The parameters to pass to the function, defaulting to + no parameters. :type data: Iterable """ # convert to list - params = list(data) + # params = list(data) not necessary will always be a json string # Lookup the method in the global namespace # Much much faster than using eval() - globals()[method](*params) + + # data should be None or always a list of params + if data is None: + globals()[method]() + else: + # one shot json decode + params = json.loads(data) + globals()[method](*params) def shutdown(self): """ diff --git a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js index 0e1e8f7ce5..42c559c999 100644 --- a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js @@ -49,8 +49,8 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta _self.updateState(data) $scope.$apply() break - case 'onNewState': - $scope.current = data + case 'onStateChange': + $scope.current = data.current $scope.$apply() break default: @@ -60,7 +60,7 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta } - msg.subscribe("publishNewState") + msg.subscribe("publishStateChange") msg.subscribe(this) } ]) diff --git a/src/main/resources/resource/WebGui/app/service/js/WebXRGui.js b/src/main/resources/resource/WebGui/app/service/js/WebXRGui.js new file mode 100644 index 0000000000..52fd02c526 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/WebXRGui.js @@ -0,0 +1,43 @@ +angular.module('mrlapp.service.WebXRGui', []).controller('WebXRGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('WebXRGuiCtrl') + var _self = this + var msg = this.msg + $scope.poses = {} + $scope.events = {} + $scope.jointAngles = {} + + this.updateState = function(service) { + $scope.service = service + } + + this.onMsg = function(inMsg) { + let data = inMsg.data[0] + switch (inMsg.method) { + case 'onState': + _self.updateState(data) + $scope.$apply() + break + case 'onPose': + $scope.poses[data.name] = data + $scope.$apply() + break + case 'onEvent': + $scope.events[data.uuid] = data + $scope.$apply() + break + case 'onJointAngles': + $scope.jointAngles = {...$scope.jointAngles, ...data} + $scope.$apply() + break + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + } + + msg.subscribe('publishPose') + msg.subscribe('publishEvent') + msg.subscribe('publishJointAngles') + msg.subscribe(this) +} +]) diff --git a/src/main/resources/resource/WebGui/app/service/views/WebXRGui.html b/src/main/resources/resource/WebGui/app/service/views/WebXRGui.html new file mode 100644 index 0000000000..ff09de61bc --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/WebXRGui.html @@ -0,0 +1,64 @@ +
+ +
Controllers
+ + + + + + + + + + + + + + + + + + + + + + + +
namexyzpitchrollyaw
{{pose.name}}{{pose.position.x.toFixed(2)}}{{pose.position.y.toFixed(2)}}{{pose.position.z.toFixed(2)}}{{pose.orientation.pitch.toFixed(2)}}{{pose.orientation.roll.toFixed(2)}}{{pose.orientation.yaw.toFixed(2)}}

+ +
Events
+ + + + + + + + + + + + + + + + + +
idhandednesstypevalue
{{event.id}}{{event.meta.handedness}}{{event.type}}{{event.value}}

+ +
Joint Angles
+ + + + + + + + + + + + + +
jointangle
{{key}}{{value.toFixed(2)}}
+
\ No newline at end of file diff --git a/src/main/resources/resource/WebXR.png b/src/main/resources/resource/WebXR.png new file mode 100644 index 0000000000000000000000000000000000000000..2432b77c92282b7a4f0f27c73b8556b998f14f19 GIT binary patch literal 20829 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 literal 0 HcmV?d00001 diff --git a/src/test/java/org/myrobotlab/service/OpenCVTest.java b/src/test/java/org/myrobotlab/service/OpenCVTest.java index 458cfef8ad..c821a47c02 100644 --- a/src/test/java/org/myrobotlab/service/OpenCVTest.java +++ b/src/test/java/org/myrobotlab/service/OpenCVTest.java @@ -95,37 +95,6 @@ public static void setUpBeforeClass() throws Exception { long ts = System.currentTimeMillis(); cv = (OpenCV) Runtime.start("cv", "OpenCV"); - /* - * - * log. - * warn("========= OpenCVTest - setupbefore class - started cv {} ms =========" - * , System.currentTimeMillis()-ts ); ts = System.currentTimeMillis(); log. - * warn("========= OpenCVTest - setupbefore class - starting capture =========" - * ); cv.capture(TEST_LOCAL_FACE_FILE_JPEG); log. - * warn("========= OpenCVTest - setupbefore class - started capture {} ms =========" - * , System.currentTimeMillis()-ts ); ts = System.currentTimeMillis(); log. - * warn("========= OpenCVTest - setupbefore class - starting getFaceDetect =========" - * ); cv.getFaceDetect(120000);// two minute wait to load all libraries log. - * warn("========= OpenCVTest - setupbefore class - started getFaceDetect {} ms =========" - * , System.currentTimeMillis()-ts ); ts = System.currentTimeMillis(); log. - * warn("========= OpenCVTest - setupbefore class - starting getClassifications =========" - * ); cv.reset(); OpenCVFilter yoloFilter = cv.addFilter("yolo"); // - * cv.getClassifications(120000); cv.capture(TEST_LOCAL_FACE_FILE_JPEG); - * log. - * warn("========= OpenCVTest - setupbefore class - started getClassifications {} ms =========" - * , System.currentTimeMillis()-ts ); - * - * ts = System.currentTimeMillis(); log. - * warn("========= OpenCVTest - setupbefore class - starting getOpenCVData =========" - * ); - * - * cv.reset(); cv.capture(TEST_LOCAL_MP4); cv.getOpenCVData(); log. - * warn("========= OpenCVTest - setupbefore class - started getOpenCVData {} ms =========" - * , System.currentTimeMillis()-ts ); cv.disableAll(); // if (!isHeadless()) - * { - no longer needed I believe - SwingGui now handles it - * - * // } - */ } // FIXME - do the following test diff --git a/src/test/java/org/myrobotlab/service/WebGuiSocketTest.java b/src/test/java/org/myrobotlab/service/WebGuiSocketTest.java new file mode 100644 index 0000000000..5a57823b8b --- /dev/null +++ b/src/test/java/org/myrobotlab/service/WebGuiSocketTest.java @@ -0,0 +1,137 @@ +package org.myrobotlab.service; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.framework.MRLListener; +import org.myrobotlab.framework.Service; +import org.myrobotlab.logging.LoggerFactory; +import org.slf4j.Logger; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +public class WebGuiSocketTest { + + protected final static Logger log = LoggerFactory.getLogger(WebGuiSocketTest.class); + + protected WebSocket webSocket; + protected WebSocketListener webSocketListener; + protected BlockingQueue msgQueue = new LinkedBlockingQueue<>(); + protected WebGui webgui2; + + @Before + public void setUp() { + webgui2 = (WebGui) Runtime.create("webgui2", "WebGui"); + webgui2.autoStartBrowser(false); + webgui2.setPort(8889); + webgui2.startService(); + + Service.sleep(3); + OkHttpClient okHttpClient = new OkHttpClient(); + Request request = new Request.Builder() + .url("ws://localhost:8889/api/messages?user=root&pwd=pwd&session_id=2309adf3dlkdk&id=webgui-client").build(); + webSocketListener = new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, okhttp3.Response response) { + // WebSocket connection is established + log.info("onOpen"); + } + + @Override + public void onMessage(WebSocket webSocket, String msg) { + log.info("onMessage {}", msg); + msgQueue.add(msg); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) { + // Handle WebSocket failure + log.info("onFailure"); + } + }; + + webSocket = okHttpClient.newWebSocket(request, webSocketListener); + } + + @After + public void teardown() { + webSocket.cancel(); + } + + @Test + public void testWebSocketConnection() throws InterruptedException { + // Use a CountDownLatch to wait for the WebSocket connection to be + // established + // CountDownLatch latch = new CountDownLatch(1); + // webSocket.listener().onOpen(webSocket, null); + // Wait for the connection to be established + // assertTrue("WebSocket connection timeout", latch.await(5, TimeUnit.SECONDS)); + + + // if sucessfully connected we'll get an + // 1. addListener from its runtime for describe + // 2. then a describe is sent with a parameter that describes the requesting platform + + String json = msgQueue.poll(5, TimeUnit.SECONDS); + LinkedHashMapmsg = (LinkedHashMap)CodecUtils.fromJson(json); + assertEquals("runtime", msg.get("name")); + assertEquals("addListener", msg.get("method")); + Object data = msg.get("data"); + List p0 = (List)msg.get("data"); + MRLListener listener = CodecUtils.fromJson((String)p0.get(0), MRLListener.class); + assertEquals("describe", listener.topicMethod); + assertEquals("onDescribe", listener.callbackMethod); + assertEquals(String.format("runtime@%s", webgui2.getId()), listener.callbackName); + + // the client can optionally do the same thing + // send an addListener for describe + // then send a describe + + String addListener = "{\n" + + " \"msgId\": 1690173331106,\n" + + " \"name\": \"runtime\",\n" + + " \"method\": \"addListener\",\n" + + " \"sender\": \"runtime@p1\",\n" + + " \"sendingMethod\": \"sendTo\",\n" + + " \"data\": [\n" + + " \"\\\"describe\\\"\",\n" + + " \"\\\"runtime@p1\\\"\"\n" + + " ],\n" + + " \"encoding\": \"json\"\n" + + "}"; + + // FIXME - make describe + // String describe = + + // assert describe info + + //.info(json); + log.info("here"); + + } + + @Test + public void testWebSocketMessage() throws InterruptedException { + // Use a CountDownLatch to wait for the WebSocket message +// CountDownLatch latch = new CountDownLatch(1); +// +// String expectedMessage = "Hello, WebSocket!"; +// webSocket.listener().onMessage(webSocket, expectedMessage); +// +// // Wait for the message to be received +// assertTrue("WebSocket message timeout", latch.await(5, TimeUnit.SECONDS)); + } + +}