diff --git a/.gitignore b/.gitignore index fb07e725f0..4fa1d43508 100644 --- a/.gitignore +++ b/.gitignore @@ -116,4 +116,7 @@ build/ /lastRestart.py /.factorypath start.yml +config +src/main/resources/resource/InMoov2 +src/main/resources/resource/ProgramAB *.iml diff --git a/README.md b/README.md index 02fc3ea8aa..0285ebd679 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ Enjoy the code review, address issues and concern in the code review Reviewer merges pull request to develop. Reviewer deletes branch. - The following config should be useful to work directly on WebGui UI and InMoov2 UI if the repos are checked out at the same level ```yml diff --git a/pom.xml b/pom.xml index 044205c9e9..c179fc989c 100644 --- a/pom.xml +++ b/pom.xml @@ -163,10 +163,6 @@ - - - - org.boofcv @@ -1237,13 +1233,13 @@ org.bytedeco cpython-platform - 3.11.3-1.5.9 + 3.10.8-1.5.8 provided org.bytedeco cpython - 3.11.3-1.5.9 + 3.10.8-1.5.8 provided @@ -1574,6 +1570,34 @@ + + + io.vertx + vertx-core + 4.3.3 + provided + + + io.netty + * + + + + + io.vertx + vertx-web + 4.3.3 + provided + + + io.netty + * + + + + + + diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index ff123320fc..6812d8d096 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -794,7 +794,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/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java new file mode 100644 index 0000000000..f87e9b9e4c --- /dev/null +++ b/src/main/java/org/myrobotlab/service/Vertx.java @@ -0,0 +1,135 @@ +package org.myrobotlab.service; + +import java.util.HashMap; +import java.util.Set; + +import org.myrobotlab.framework.Service; +import org.myrobotlab.logging.Level; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.vertx.ApiVerticle; +import org.slf4j.Logger; + +import io.vertx.core.VertxOptions; + +/** + * 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 greg + * + */ +public class Vertx extends Service { + + 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(); + } + + + /** + * + */ + public void stop() { + log.info("stopping driver"); + Set ids = vertx.deploymentIDs(); + for (String id : ids) { + vertx.undeploy(id, (result) -> { + if (result.succeeded()) { + log.info("succeeded"); + } else { + log.error("failed"); + } + }); + } + } + + public static class Matrix { + public String name; + public HashMap matrix; + + public Matrix() { + }; + } + + public Matrix publishMatrix(Matrix data) { + // log.info("publishMatrix {}", data.name); + return data; + } + + 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); + } + } +} 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..d194df1e4e --- /dev/null +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -0,0 +1,89 @@ +package org.myrobotlab.service; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.framework.Service; +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.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 Pose publishPose(Pose pose) { + log.warn("publishPose {}", pose); + System.out.println(pose.toString()); + + // process mappings config into joint angles + Map map = new HashMap<>(); + + WebXRConfig c = (WebXRConfig)config; + String path = String.format("%s.orientation.roll", pose.name); + if (c.mappings.containsKey(path)) { + Map mapper = c.mappings.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 (c.mappings.containsKey(path)) { + Map mapper = c.mappings.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 (c.mappings.containsKey(path)) { + Map mapper = c.mappings.get(path); + for (String name: mapper.keySet()) { + map.put(name, mapper.get(name).calcOutput(pose.orientation.yaw)); + } + } + + invoke("publishJointAngles", map); + + // TODO - publishQuaternion + // invoke("publishQuaternion", map); + + return pose; + } + + + public Map publishJointAngles(Map map){ + return map; + } + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + 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/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/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/meta/WebXRMeta.java b/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java new file mode 100644 index 0000000000..171959fbf5 --- /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..6b3ca595c7 --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -0,0 +1,105 @@ +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.service.Runtime; +import org.myrobotlab.service.config.VertxConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.ServerWebSocket; +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 + + //StaticHandler root = StaticHandler.create("src/main/resources/resource/Vertx/app"); + // StaticHandler root = StaticHandler.create("src/main/resources/resource/Vertx/app"); + StaticHandler root = StaticHandler.create("../robotlab-x-app/build/"); + root.setCachingEnabled(false); + root.setDirectoryListing(true); + root.setIndexPage("index.html"); + // root.setAllowRootFileSystemAccess(true); + // root.setWebRoot(null); + 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..76d0fb8c1f --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java @@ -0,0 +1,93 @@ +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.service.Runtime; +import org.slf4j.Logger; + +import io.vertx.core.Handler; +import io.vertx.core.http.ServerWebSocket; + +/** + * + * 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); + + transient private org.myrobotlab.service.Vertx service = null; + 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; + } + + 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 + // } + + } 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 event) { + + // ctx.writeTextMessage("ping"); FIXME - query ? + // FIXME - thread-safe ? how many connections mapped to objects ? + event.textMessageHandler(new TextMessageHandler(service)); + + } + +}