From e07d8b07ec8f2f885dcee5cb3b2197e21bf4c5ef Mon Sep 17 00:00:00 2001 From: supertick Date: Sat, 18 Mar 2023 20:31:28 -0700 Subject: [PATCH 001/232] wip webxr vertx --- .../java/org/myrobotlab/service/WebXr.java | 52 ++++++++++++++++++- .../service/config/WebXrConfig.java | 30 ++++++++++- .../myrobotlab/service/data/Orientation.java | 1 + .../org/myrobotlab/service/data/Pose.java | 22 ++++++++ .../org/myrobotlab/service/data/Position.java | 43 +++++++++++++++ .../org/myrobotlab/vertx/ApiVerticle.java | 3 +- 6 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/myrobotlab/service/data/Pose.java create mode 100644 src/main/java/org/myrobotlab/service/data/Position.java diff --git a/src/main/java/org/myrobotlab/service/WebXr.java b/src/main/java/org/myrobotlab/service/WebXr.java index 80be870ff7..87df997800 100644 --- a/src/main/java/org/myrobotlab/service/WebXr.java +++ b/src/main/java/org/myrobotlab/service/WebXr.java @@ -1,11 +1,15 @@ 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.service.config.ServiceConfig; +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 { @@ -17,7 +21,50 @@ public class WebXr extends Service { 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); + + return pose; + } + + // TODO publishQuaternion + public Map publishJointAngles(Map map){ + return map; + } + public static void main(String[] args) { try { @@ -28,6 +75,9 @@ public static void main(String[] args) { // 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) { diff --git a/src/main/java/org/myrobotlab/service/config/WebXrConfig.java b/src/main/java/org/myrobotlab/service/config/WebXrConfig.java index ebe4ae4c9e..c97615440b 100644 --- a/src/main/java/org/myrobotlab/service/config/WebXrConfig.java +++ b/src/main/java/org/myrobotlab/service/config/WebXrConfig.java @@ -1,9 +1,37 @@ package org.myrobotlab.service.config; +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.math.MapperSimple; + public class WebXrConfig extends ServiceConfig { public Integer port = 8888; public boolean autoStartBrowser = true; - public boolean enableMdns = false; + /** + * range and name mappings for orientation and position + * controller name | servo name | mapping + */ + public Map> mappings = new HashMap<>(); + + public WebXrConfig() { + + Map map = new HashMap<>(); + map.put("i01.head.rollNeck", new MapperSimple(-3.14, 3.14, -90, 270)); + mappings.put("head.orientation.roll", map); + + map = new HashMap<>(); + map.put("i01.head.rothead", new MapperSimple(-3.14, 3.14, -90, 270)); + mappings.put("head.orientation.yaw", map); + + map = new HashMap<>(); + map.put("i01.head.neck", new MapperSimple(-3.14, 3.14, -90, 270)); + mappings.put("head.orientation.pitch", map); + + } + } + + 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/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java index 3f101095d0..6b3ca595c7 100644 --- a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -55,7 +55,8 @@ public void start() throws Exception { // 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("src/main/resources/resource/Vertx/app"); + StaticHandler root = StaticHandler.create("../robotlab-x-app/build/"); root.setCachingEnabled(false); root.setDirectoryListing(true); root.setIndexPage("index.html"); From 2332822209530a77b0a22ee7981de792d12fa766 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 16 Jul 2023 18:51:59 -0700 Subject: [PATCH 002/232] cron update --- .../java/org/myrobotlab/service/Clock.java | 2 +- .../java/org/myrobotlab/service/Cron.java | 290 ++++++++++++------ .../myrobotlab/service/config/CronConfig.java | 11 + src/main/resources/resource/RasPi/diagram.png | Bin 0 -> 75037 bytes .../WebGui/app/service/js/ClockGui.js | 3 +- .../resource/WebGui/app/service/js/CronGui.js | 53 ++++ .../WebGui/app/service/views/ClockGui.html | 9 - .../WebGui/app/service/views/CronGui.html | 57 ++++ 8 files changed, 313 insertions(+), 112 deletions(-) create mode 100644 src/main/java/org/myrobotlab/service/config/CronConfig.java create mode 100644 src/main/resources/resource/RasPi/diagram.png create mode 100644 src/main/resources/resource/WebGui/app/service/js/CronGui.js create mode 100644 src/main/resources/resource/WebGui/app/service/views/CronGui.html diff --git a/src/main/java/org/myrobotlab/service/Clock.java b/src/main/java/org/myrobotlab/service/Clock.java index aefe9fdf03..df43ed6745 100644 --- a/src/main/java/org/myrobotlab/service/Clock.java +++ b/src/main/java/org/myrobotlab/service/Clock.java @@ -74,7 +74,7 @@ synchronized public void stop() { thread.interrupt(); broadcastState(); } else { - log.info("{} already stopped"); + log.info("{} already stopped", getName()); } c.running = false; Service.sleep(20); diff --git a/src/main/java/org/myrobotlab/service/Cron.java b/src/main/java/org/myrobotlab/service/Cron.java index fd2ca9d064..b25f44bdcc 100644 --- a/src/main/java/org/myrobotlab/service/Cron.java +++ b/src/main/java/org/myrobotlab/service/Cron.java @@ -1,44 +1,70 @@ package org.myrobotlab.service; import java.io.Serializable; -import java.util.ArrayList; +import java.util.Map; +import java.util.UUID; -import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.CronConfig; import org.slf4j.Logger; import it.sauronsoftware.cron4j.Scheduler; /** - * Cron - This is a cron based service that can execute a "task" at some point - * in the future such as "invoke this method on that service" + * Cron - This is a cron based service that can execute a "task". * - * FIXME - the common cron notation is kind of nice - but this thing doesn't do - * more than Service.addTask - * - * FIXME - make a purge & delete DUH ! - * */ public class Cron extends Service { public static class Task implements Serializable, Runnable { + private static final long serialVersionUID = 1L; - transient Cron myService; + /** + * cron pattern for this task + */ public String cronPattern; - public String name; - public String method; + + /** + * data parameters to invoke + */ public Object[] data; - public Task(Cron myService, String cronPattern, String name, String method) { - this(myService, cronPattern, name, method, (Object[]) null); + /** + * unique hash the scheduler uses (only) + */ + transient public String hash; + + /** + * unique id for the user to use + */ + public String id; + + /** + * method to invoke + */ + public String method; + + transient Cron cron; + + /** + * name of the target service + */ + public String name; + + public Task() { } - public Task(Cron myService, String cronPattern, String name, String method, Object... data) { - this.myService = myService; + public Task(Cron cron, String id, String cronPattern, String name, String method) { + this(cron, id, cronPattern, name, method, (Object[]) null); + } + + public Task(Cron cron, String id, String cronPattern, String name, String method, Object... data) { + this.cron = cron; + this.id = id; this.cronPattern = cronPattern; this.name = name; this.method = method; @@ -47,128 +73,190 @@ public Task(Cron myService, String cronPattern, String name, String method, Obje @Override public void run() { - log.info("{} Cron firing message {}->{}.{}", myService.getName(), name, method, data); - myService.send(name, method, data); + log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data); + cron.send(name, method, data); + } + + @Override + public String toString() { + return String.format("%s, %s, %s, %s", id, cronPattern, name, method); } } - private static final long serialVersionUID = 1L; + public final static Logger log = LoggerFactory.getLogger(Cron.class); - public final static Logger log = LoggerFactory.getLogger(Cron.class.getCanonicalName()); + private static final long serialVersionUID = 1L; + /** + * the thing that translates all the cron pattern values and implements actual tasks + */ transient private Scheduler scheduler = new Scheduler(); - // Schedule a once-a-week task at 8am on Sunday. - // 0 8 * * 7 - // Schedule a twice a day task at 7am and 6pm on weekdays - // 0 7 * * 1-5 |0 18 * * 1-5 - - public final static String EVERY_MINUTE = "* * * * *"; - - public ArrayList tasks = new ArrayList(); - - public static void main(String[] args) { - LoggingFactory.init(Level.INFO); - - try { - Cron cron = (Cron) Runtime.start("cron", "Cron");// new - // Cron("cron"); - cron.startService(); - - /* - * cron.addScheduledEvent("0 6 * * 1,3,5","arduino","digitalWrite", 13, - * 1); cron.addScheduledEvent("0 7 * * 1,3,5","arduino","digitalWrite", - * 12, 1); cron.addScheduledEvent("0 8 * * 1,3,5" - * ,"arduino","digitalWrite", 11, 1); - * - * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 13, 0); - * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 12, 0); - * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0); - */ - cron.addTask("* * * * *", "cron", "test", 7); - - // cron.addScheduledEvent(EVERY_MINUTE, "log", "log"); - // west wall | back | east wall - - String json = CodecUtils.toJson(cron.getTasks()); - - log.info("here {}", json); - - // Runtime.createAndStart("webgui", "WebGui"); - - // 1. doug - find location where checked in ---- - // 2. take out security token from DL broker's response - // 3. Tony - status ? and generated xml responses - "update" looks - // ok - - // Runtime.createAndStart("gui", "SwingGui"); - /* - * SwingGui gui = new SwingGui("gui"); gui.startService(); - */ - } catch (Exception e) { - Logging.logError(e); - } - } - public Cron(String n, String id) { super(n, id); } - /* - * addTask - Add a task to the cron service to invoke a method on a service on - * some schedule. - * - * @param cron - The cron string to define the schedule - * - * @param serviceName - The name of the service to invoke + /** + * Add a named task with out parameters * - * @param method - the method on the service to invoke when the task starts. + * @param id + * @param cron + * @param serviceName + * @param method + * @return */ - public String addTask(String cron, String serviceName, String method) { - return addTask(cron, serviceName, method, (Object[]) null); + public String addNamedTask(String id, String cron, String serviceName, String method) { + return addNamedTask(id, cron, serviceName, method, (Object[]) null); } - /* - * addTask - Add a task to the cron service to invoke a method on a service on - * some schedule. + /** + * Add a named task with parameters * - * @param cron - The cron string to define the schedule + * @param id + * @param cron + * @param serviceName + * @param method + * @param data + * @return + */ + public String addNamedTask(String id, String cron, String serviceName, String method, Object... data) { + CronConfig c = (CronConfig) config; + Task task = new Task(this, id, cron, serviceName, method, data); + task.id = id; + task.hash = scheduler.schedule(cron, task); + c.tasks.put(id, task); + broadcastState(); + return id; + } + + /** * - * @param serviceName - The name of the service to invoke + * @param task + * @return + */ + public String addNamedTask(Task task) { + CronConfig c = (CronConfig) config; + task.hash = scheduler.schedule(task.cronPattern, task); + c.tasks.put(task.id, task); + broadcastState(); + return task.id; + } + + /** + * Add a task with out parameters, the name will be generated guid * - * @param method - the method on the service to invoke when the task starts. + * @param cron + * @param serviceName + * @param method + * @return + */ + public String addTask(String cron, String serviceName, String method) { + String id = UUID.randomUUID().toString(); + return addNamedTask(id, cron, serviceName, method, (Object[]) null); + } + + /** + * Add a task with parameters, the name will be generated guid * - * @param data - additional objects/varags to pass to the method + * @param cron + * @param serviceName + * @param method + * @param data + * @return */ public String addTask(String cron, String serviceName, String method, Object... data) { - Task task = new Task(this, cron, serviceName, method, data); - tasks.add(task); - return scheduler.schedule(cron, task); + String id = UUID.randomUUID().toString(); + return addNamedTask(id, cron, serviceName, method, data); } - public ArrayList getCronTasks() { - return tasks; + public Map getCronTasks() { + CronConfig c = (CronConfig) config; + return c.tasks; + } + + /** + * removes task by id + * @param id - id of the task to remove + * @return the removed task if it exists + */ + public Task removeTask(String id) { + CronConfig c = (CronConfig) config; + Task t = c.tasks.remove(id); + if (t != null) { + scheduler.deschedule(t.hash); + } else { + log.error("%s could not find task %s to remove", getName(), id); + } + broadcastState(); + return t; + } + + /** + * removes all the tasks without stopping the scheduler + */ + public void removeAllTasks() { + CronConfig c = (CronConfig) config; + for (Task t : c.tasks.values()) { + scheduler.deschedule(t.hash); + } + c.tasks.clear(); } @Override public void startService() { super.startService(); - if (!scheduler.isStarted()) { - scheduler.start(); - } + start(); } @Override public void stopService() { super.stopService(); + stop(); + } + + /** + * start the schedular and all associated tasks + */ + public void start() { + if (!scheduler.isStarted()) { + scheduler.start(); + } + } + + /** + * stop the schedular ad all associated tasks + */ + public void stop() { if (scheduler.isStarted()) { scheduler.stop(); } } - public int test(Integer data) { - log.info("data {}", data); - return data; - } + public static void main(String[] args) { + LoggingFactory.init(Level.INFO); + + try { + Cron cron = (Cron) Runtime.start("cron", "Cron"); + Arduino mega = (Arduino) Runtime.start("mega", "Arduino"); + mega.connect("/dev/ttyACM2"); + + Runtime.start("webgui", "WebGui"); + /* + * + * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 13, 0); + * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 12, 0); + * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0); + */ + // every odd minute + String id = cron.addNamedTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1); + // every event minute + String id2 = cron.addNamedTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0); + + // Runtime.createAndStart("webgui", "WebGui"); + + } catch (Exception e) { + Logging.logError(e); + } + } } diff --git a/src/main/java/org/myrobotlab/service/config/CronConfig.java b/src/main/java/org/myrobotlab/service/config/CronConfig.java new file mode 100644 index 0000000000..5bb0c50ff5 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/CronConfig.java @@ -0,0 +1,11 @@ +package org.myrobotlab.service.config; + +import java.util.LinkedHashMap; + +import org.myrobotlab.service.Cron.Task; + +public class CronConfig extends ServiceConfig { + + public LinkedHashMap tasks = new LinkedHashMap<>(); + +} diff --git a/src/main/resources/resource/RasPi/diagram.png b/src/main/resources/resource/RasPi/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c37c17c7766dce9e56dcfa98ae87bda897daa0bc GIT binary patch literal 75037 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Ξh4rv0oYEB*M#3LygQFjruT-zhZ(;c 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

GznH5S5!WvV!v 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 Ef(|r6!~g&Q literal 0 HcmV?d00001 diff --git a/src/main/resources/resource/WebGui/app/service/js/ClockGui.js b/src/main/resources/resource/WebGui/app/service/js/ClockGui.js index b5f4202fe6..46eaf8f62b 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ClockGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ClockGui.js @@ -20,7 +20,8 @@ angular.module('mrlapp.service.ClockGui', []).controller('ClockGuiCtrl', ['$scop $scope.$apply() break case 'onTime': - $scope.onTime = data + const date = new Date(data); + $scope.onTime = date.toLocaleString(); $scope.$apply() break case 'onEpoch': diff --git a/src/main/resources/resource/WebGui/app/service/js/CronGui.js b/src/main/resources/resource/WebGui/app/service/js/CronGui.js new file mode 100644 index 0000000000..80ce7a109b --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/CronGui.js @@ -0,0 +1,53 @@ +angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope', 'mrl', function($scope, mrl) { + console.info('CronGuiCtrl') + var _self = this + var msg = this.msg + + // str verson of parameters from the input + // text form field + $scope.parameters = null + + $scope.newTask = { + id: null, + cronPattern: null, + name: null, + method: null, + data: null + } + + // GOOD TEMPLATE TO FOLLOW + 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 + default: + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } + } + + $scope.addNamedTask = function() { + if ($scope.parameters && $scope.parameters.length > 0){ + $scope.newTask.data = JSON.parse($scope.parameters) + } else { + $scope.newTask.data = null + } + + msg.send('addNamedTask', $scope.newTask) + } + + $scope.removeTask = function(id) { + msg.send('removeTask', id) + } + + + msg.subscribe(this) +} +]) diff --git a/src/main/resources/resource/WebGui/app/service/views/ClockGui.html b/src/main/resources/resource/WebGui/app/service/views/ClockGui.html index 3a518ac09f..4107890158 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ClockGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ClockGui.html @@ -1,9 +1,7 @@

- time

{{onTime}}

- epoch in ms

{{onEpoch}}

@@ -14,10 +12,3 @@

{{onEpoch}}

- - - - - diff --git a/src/main/resources/resource/WebGui/app/service/views/CronGui.html b/src/main/resources/resource/WebGui/app/service/views/CronGui.html new file mode 100644 index 0000000000..78f17783cb --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/CronGui.html @@ -0,0 +1,57 @@ +
+

Cron Tab

+ + cron help + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namecronservicemethodparameters
+ + {{ value.id }}{{ value.cronPattern }}{{ value.name }}{{ value.method }}{{ value.data }}
+ + + + + + + + + +
+ +
+
+
From 06783cb70fc1747d4540e1e71fe94a2b14774842 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 16 Jul 2023 19:42:34 -0700 Subject: [PATCH 003/232] raspi updates --- .../java/org/myrobotlab/service/RasPi.java | 656 ++++++++++++------ .../service/config/RasPiConfig.java | 7 +- .../WebGui/app/service/js/RasPiGui.js | 29 +- .../WebGui/app/service/views/RasPiGui.html | 48 +- 4 files changed, 497 insertions(+), 243 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index ef1475a7fc..fd57408e16 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -17,9 +17,9 @@ import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.i2c.I2CFactory; import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.abstracts.AbstractMicrocontroller; +import org.myrobotlab.service.config.RasPiConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; @@ -31,7 +31,7 @@ import com.pi4j.io.gpio.GpioPinDigitalMultipurpose; import com.pi4j.io.gpio.Pin; import com.pi4j.io.gpio.PinMode; -import com.pi4j.io.gpio.PinPullResistance; +import com.pi4j.io.gpio.PinState; import com.pi4j.io.gpio.RaspiPin; import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent; import com.pi4j.io.gpio.event.GpioPinListenerDigital; @@ -51,12 +51,6 @@ */ public class RasPi extends AbstractMicrocontroller implements I2CController, GpioPinListenerDigital { - @Override - public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) { - // display pin state on console - log.info(" --> GPIO PIN STATE CHANGE: {} = {}", event.getPin(), event.getState()); - } - public static class I2CDeviceMap { transient public I2CBus bus; transient public I2CDevice device; @@ -68,10 +62,7 @@ public String toString() { } } - /** - * default bus current bus of raspi service - */ - protected String bus = "1"; + public final static Map bcmToWiring = new HashMap<>(); public static final int INPUT = 0x0; @@ -81,48 +72,94 @@ public String toString() { private static final long serialVersionUID = 1L; - protected transient GpioController gpio; + public final static Map wiringToBcm = new HashMap<>(); + + static { + + bcmToWiring.put("GPIO 0", "GPIO 27"); + bcmToWiring.put("GPIO 1", "GPIO 31"); + bcmToWiring.put("GPIO 2", "GPIO 8"); + bcmToWiring.put("GPIO 3", "GPIO 9"); + bcmToWiring.put("GPIO 4", "GPIO 7"); + bcmToWiring.put("GPIO 5", "GPIO 21"); + bcmToWiring.put("GPIO 6", "GPIO 22"); + bcmToWiring.put("GPIO 7", "GPIO 11"); + bcmToWiring.put("GPIO 8", "GPIO 10"); + bcmToWiring.put("GPIO 9", "GPIO 13"); + bcmToWiring.put("GPIO 10", "GPIO 12"); + bcmToWiring.put("GPIO 11", "GPIO 14"); + bcmToWiring.put("GPIO 12", "GPIO 27"); + bcmToWiring.put("GPIO 13", "GPIO 26"); + bcmToWiring.put("GPIO 14", "GPIO 15"); + bcmToWiring.put("GPIO 15", "GPIO 16"); + bcmToWiring.put("GPIO 16", "GPIO 25"); + bcmToWiring.put("GPIO 17", "GPIO 0"); + bcmToWiring.put("GPIO 18", "GPIO 1"); + bcmToWiring.put("GPIO 19", "GPIO 23"); + bcmToWiring.put("GPIO 20", "GPIO 28"); + bcmToWiring.put("GPIO 21", "GPIO 29"); + bcmToWiring.put("GPIO 22", "GPIO 3"); + bcmToWiring.put("GPIO 23", "GPIO 4"); + bcmToWiring.put("GPIO 24", "GPIO 5"); + bcmToWiring.put("GPIO 25", "GPIO 6"); + bcmToWiring.put("GPIO 26", "GPIO 24"); + bcmToWiring.put("GPIO 27", "GPIO 2"); + + for (String pin : bcmToWiring.keySet()) { + String wiring = bcmToWiring.get(pin); + wiringToBcm.put(wiring, pin); + } + } + + public static void main(String[] args) { + LoggingFactory.init("info"); + + /* + * RasPi.displayString(1, 70, "1"); + * + * RasPi.displayString(1, 70, "abcd"); + * + * RasPi.displayString(1, 70, "1234"); + * + * + * //RasPi raspi = new RasPi("raspi"); + */ + + // raspi.writeDisplay(busAddress, deviceAddress, data) + + int i = 0; + + Runtime.start("servo01", "Servo"); + Runtime.start("ada16", "Adafruit16CServoDriver"); + Runtime.start(String.format("rasPi%d", i), "RasPi"); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + + } + + protected String boardType = null; - protected Map> validAddresses = new HashMap<>(); + /** + * default bus current bus of raspi service + */ + protected String bus = "1"; + + protected transient GpioController gpio; /** * for attached devices */ protected Map i2cDevices = new HashMap(); - protected String boardType = null; + protected Map> validI2CAddresses = new HashMap<>(); + + protected String wrongPlatformError = null; public RasPi(String n, String id) { super(n, id); - - Platform platform = Platform.getLocalInstance(); - log.info("platform is {}", platform); - log.info("architecture is {}", platform.getArch()); - - try { - boardType = SystemInfo.getBoardType().toString(); - gpio = GpioFactory.getInstance(); - log.info("Executing on Raspberry PI"); - getPinList(); - } catch (Exception e) { - // an error in the constructor won't get broadcast - so we need Runtime to - // do it - Runtime.getInstance().error("raspi service requires arm %s is not arm - %s", getName(), e.getMessage()); - } } - /* - * @Override public void attach(String name) { ServiceInterface si = - * Runtime.getService(name); if - * (I2CControl.class.isAssignableFrom(si.getClass())) { - * attachI2CControl((I2CControl) si); return; } } - * - * @Override public void detach(String name) { ServiceInterface si = - * Runtime.getService(name); if - * (I2CControl.class.isAssignableFrom(si.getClass())) { - * detachI2CControl((I2CControl) si); return; } } - */ - @Override public void attach(Attachable service) throws Exception { if (I2CControl.class.isAssignableFrom(service.getClass())) { @@ -131,14 +168,6 @@ public void attach(Attachable service) throws Exception { } } - @Override - public void detach(Attachable service) { - if (I2CControl.class.isAssignableFrom(service.getClass())) { - detachI2CControl((I2CControl) service); - return; - } - } - @Override public void attachI2CControl(I2CControl control) { @@ -176,6 +205,14 @@ void createI2cDevice(int bus, int address, String serviceName) { } } + @Override + public void detach(Attachable service) { + if (I2CControl.class.isAssignableFrom(service.getClass())) { + detachI2CControl((I2CControl) service); + return; + } + } + @Override public void detachI2CControl(I2CControl control) { // This method should delete the i2c device entry from the list of @@ -190,42 +227,66 @@ public void detachI2CControl(I2CControl control) { } } - public void digitalWrite(int pin, int value) { - log.info("digitalWrite {} {}", pin, value); - // msg.digitalWrite(pin, value); - PinDefinition pinDef = addressIndex.get(pin); - GpioPinDigitalMultipurpose gpio = ((GpioPinDigitalMultipurpose) pinDef.getPinImpl()); - if (value == 0) { - gpio.low(); - } else { - gpio.high(); - } - invoke("publishPinDefinition", pinDef); + @Override + @Deprecated /* use disablePin(String) */ + public void disablePin(int address) { + error("disablePin(int) not supported use disablePin(String)"); } @Override - public void disablePin(int address) { - PinDefinition pin = addressIndex.get(address); - pin.setEnabled(false); - ((GpioPinDigitalMultipurpose) pin.getPinImpl()).removeListener(); - PinDefinition pinDef = addressIndex.get(address); + public void disablePin(String pin) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + PinDefinition pinDef = pinIndex.get(pin); + pinDef.setEnabled(false); + getGPIO(pin).removeListener(this); invoke("publishPinDefinition", pinDef); } @Override + @Deprecated /* use enablePin(String pin) */ public void enablePin(int address) { - enablePin(address, 0); + error("enablePin(int address) not supoprted use enablePin(String pin)"); } @Override + @Deprecated /* use enablePin(String, int) */ public void enablePin(int address, int rate) { - PinDefinition pinDef = addressIndex.get(address); - GpioPinDigitalMultipurpose gpio = ((GpioPinDigitalMultipurpose) pinDef.getPinImpl()); - gpio.addListener(this); + error("use enablePin(String, int)"); + } + + @Override + public void enablePin(String pin) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + RasPiConfig c = (RasPiConfig) config; + enablePin(pin, c.pollRateHz); + } + + @Override + public void enablePin(String pin, int rate) { + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } + + PinDefinition pinDef = pinIndex.get(pin); + pinMode(pin, "INPUT"); + getGPIO(pin).addListener(this); pinDef.setEnabled(true); invoke("publishPinDefinition", pinDef); // broadcast pin change } + // - add more pin mappings if desired ... + @Override + public Integer getAddress(String pin) { + return Integer.parseInt(pin); + } + @Override /* services attached - not i2c devices */ public Set getAttached() { Set ret = new TreeSet<>(); @@ -235,40 +296,115 @@ public Set getAttached() { return ret; } + @Override + public BoardInfo getBoardInfo() { + + RaspiPin.allPins(); + // FIXME - this needs more work .. BoardInfo needs to be an interface where + // RasPiInfo is derived + return null; + } + + @Override + public List getBoardTypes() { + // TODO Auto-generated method stub + // FIXME - this need work + return null; + } + + /** + * Gets the multipurpose implementation of a pin, if it doesn't currently + * exists, it will provision it. + * + * @param pin + * @return + */ + private GpioPinDigitalMultipurpose getGPIO(String pin) { + log.info("getGPIO {}", pin); + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return null; + } + + PinDefinition pindef = getPin(pin); + if (pindef == null) { + error("No pin definition exists for %s", pin); + return null; + } + + GpioPinDigitalMultipurpose gpioPin = (GpioPinDigitalMultipurpose) pindef.getPinImpl(); + if (gpioPin == null) { + log.info("provisioning gpio {}", pin); + gpioPin = gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByName(bcmToWiring.get(pin)), PinMode.DIGITAL_OUTPUT); + pindef.setPinImpl(gpioPin); + } + + return gpioPin; + } + @Override public List getPinList() { + List pinList = new ArrayList<>(); - for (Pin pin : RaspiPin.allPins()) { - - // pin.getSupportedPinModes() - PinDefinition pindef = new PinDefinition(getName(), pin.getAddress()); - pindef.setPinName(pin.getName()); - EnumSet modes = pin.getSupportedPinModes(); - // FIXME - the raspi definitions are "better" they have input & ouput - // FIXME - reconcile rxtx - // FIXME - get pull up resistance - if (modes.contains(PinMode.DIGITAL_OUTPUT)) { - pindef.setDigital(true); - } - if (modes.contains(PinMode.ANALOG_OUTPUT)) { - pindef.setAnalog(true); - } - if (modes.contains(PinMode.PWM_OUTPUT)) { - pindef.setAnalog(true); + if (!pinIndex.isEmpty()) { + pinList.addAll(pinIndex.values()); + return pinList; + } + + for (Pin wiringPin : RaspiPin.allPins()) { + + // RaspiPin.allPins() RETURNS WIRING NUMBERS !!!! + + // if (wiringPin.getName().equals("GPIO 2") || + // wiringPin.getName().equals("GPIO 3") || + // wiringPin.getName().equals("GPIO 8") || + // wiringPin.getName().equals("GPIO 9")) { + // log.info("filtering out pin {} from gpio provisioning", wiringPin); + // continue; + // } + + String wPinName = wiringPin.getName(); + + if (!wiringToBcm.containsKey(wPinName)) { + log.info("skipping wiring pin {} - no gpio definition", wPinName); + continue; } - addressIndex.put(pin.getAddress(), pindef); - pinIndex.put(pin.getName(), pindef); + String bcmPinName = wiringToBcm.get(wPinName); - // GpioPinDigitalInput provisionedPin = gpio.provisionDigitalInputPin(pin, - // pull); - // provisionedPin.setShutdownOptions(true); // unexport pin on program - // shutdown - // provisionedPins.add(provisionedPin); // add provisioned pin to - // collection + PinDefinition pindef = new PinDefinition(); + // set to output for starting + pindef.setMode("OUTPUT"); + pindef.setPinName(bcmPinName); + EnumSet modes = wiringPin.getSupportedPinModes(); + + pindef.setDigital(modes.contains(PinMode.DIGITAL_OUTPUT)); + pindef.setAnalog(modes.contains(PinMode.ANALOG_OUTPUT)); + pindef.setPwm(modes.contains(PinMode.PWM_OUTPUT)); + + // FIXME - remove this, do not support address only pin + String lastPart = bcmPinName.trim().split(" ")[1]; + pindef.setAddress(Integer.parseInt(lastPart)); + + pinIndex.put(bcmPinName, pindef); + pinList.add(pindef); + } + + return pinList; + } + + @Override + public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) { + // display pin state on console + log.info(" --> GPIO PIN STATE CHANGE: {} = {}", event.getPin(), event.getState()); + PinDefinition pindef = pinIndex.get(wiringToBcm.get(event.getPin().getName())); + if (pindef == null){ + log.error("pindef is null for pin {}", event.getPin().getName()); + } else { + pindef.setValue(event.getState().getValue()); + invoke("publishPinDefinition", pindef); } - return new ArrayList(addressIndex.values()); } @Override // FIXME - I2CControl has bus why is it supplied here as a @@ -331,7 +467,7 @@ public int i2cWriteRead(I2CControl control, int busAddress, int deviceAddress, b try { devicedata.device.read(writeBuffer, 0, writeBuffer.length, readBuffer, 0, readBuffer.length); } catch (IOException e) { - Logging.logError(e); + error(e); } return readBuffer.length; } @@ -345,27 +481,37 @@ public int i2cWriteRead(I2CControl control, int busAddress, int deviceAddress, b * @param mode * INPUT = 0x0. Output = 0x1. */ - public void pinMode(int pin, int mode) { - - PinDefinition pinDef = addressIndex.get(pin); - if (mode == INPUT) { - pinDef.setPinImpl(gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByAddress(pin), PinMode.DIGITAL_INPUT)); - } else { - pinDef.setPinImpl(gpio.provisionDigitalMultipurposePin(RaspiPin.getPinByAddress(pin), PinMode.DIGITAL_OUTPUT)); + public void pinMode(String pin, String mode) { + log.info("pinMode {}, mode {}", pin, mode); + + if (mode == null) { + error("Pin mode cannot be null"); + return; } - invoke("publishPinDefinition", pinDef); - } - @Override - public void pinMode(int address, String mode) { + mode = mode.trim().toUpperCase(); + + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; + } - if (mode != null && mode.equalsIgnoreCase("INPUT")) { - pinMode(address, INPUT); + PinDefinition pinDef = pinIndex.get(pin); + // this will provision the pin if it is not already provisioned + GpioPinDigitalMultipurpose gpio = getGPIO(pin); + if (mode.equals("INPUT")) { + pinDef.setMode("INPUT"); + gpio.setMode(PinMode.DIGITAL_INPUT); + } else if (mode.equals("OUTPUT")) { + pinDef.setMode("OUTPUT"); + gpio.setMode(PinMode.DIGITAL_OUTPUT); } else { - pinMode(address, OUTPUT); + error("mode %s is not valid", mode); } + log.info("pinDef {}",pinDef); + invoke("publishPinDefinition", pinDef); } - + @Override public PinData publishPin(PinData pinData) { // TODO Make sure this method is invoked when a pin value interrupt is @@ -376,6 +522,106 @@ public PinData publishPin(PinData pinData) { return pinData; } + public void read() { + log.debug("read task invoked"); + List pinArray = new ArrayList<>(); + // load pin array + for (String pin : pinIndex.keySet()) { + PinDefinition pindef = pinIndex.get(pin); + if (pindef.isEnabled()) { + log.info("pin {} enabled {}", pin, pindef.isEnabled()); + int value = read(pin); + pindef.setValue(value); + PinData pd = new PinData(pin, value); + log.info("pin data {}", pd); + pinArray.add(pd); + } + } + + if (pinArray.size() > 0) { + PinData[] array = pinArray.toArray(new PinData[0]); + invoke("publishPinArray", new Object[]{array}); + } + } + + @Override + public int read(String pin) { + + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return -1; + } + PinDefinition pindef = pinIndex.get(pin); + GpioPinDigitalMultipurpose gpioPin = getGPIO(pin); + if (!gpioPin.isMode(PinMode.DIGITAL_INPUT)){ + pinMode(pin, "INPUT"); + } + if (gpioPin.isLow()) { + pindef.setValue(0); + return 0; + } else { + pindef.setValue(1); + return 1; + } + } + + @Override + public void reset() { + // TODO Auto-generated method stub + // reset pins/i2c devices/gpio pins + } + + public void scan() { + scan(null); + } + + public void scan(Integer busNumber) { + + if (busNumber == null) { + busNumber = Integer.parseInt(bus); + } + + try { + + I2CBus bus = I2CFactory.getInstance(busNumber); + + if (!validI2CAddresses.containsKey(busNumber)) { + validI2CAddresses.put(busNumber, new HashSet<>()); + } + + Set addresses = validI2CAddresses.get(busNumber); + + for (int i = 1; i < 128; i++) { + String hex = Integer.toHexString(i); + try { + I2CDevice device = bus.getDevice(i); + device.read(); + if (!addresses.contains(hex)) { + addresses.add(hex); + info("found new i2c device %s", hex); + } + } catch (Exception ignore) { + if (addresses.contains(hex)) { + info("removing i2c device %s", hex); + addresses.remove(hex); + } + } + } + + log.debug("scanning bus {} found: ---", busNumber); + for (String a : addresses) { + log.debug("address: " + a); + } + log.debug("----------"); + + } catch (Exception e) { + error("cannot access i2c bus %d", busNumber); + log.error("scan threw", e); + } + + broadcastState(); + } + // FIXME - return array /** * Starts a scan of the I2C specified and returns a list of addresses that @@ -417,7 +663,7 @@ public Integer[] scanI2CDevices(int busAddress) { } } } catch (Exception e) { - Logging.logError(e); + error(e); } Integer[] ret = list.toArray(new Integer[list.size()]); @@ -428,18 +674,74 @@ public Integer[] scanI2CDevices(int busAddress) { public void startService() { super.startService(); try { - log.info("Initiating i2c"); - I2CFactory.getInstance(Integer.parseInt(bus)); - log.info("i2c initiated on bus {}", bus); - // scan(); takes too long + + Platform platform = Platform.getLocalInstance(); + log.info("platform is {}", platform); + log.info("architecture is {}", platform.getArch()); + + boardType = SystemInfo.getBoardType().toString(); + gpio = GpioFactory.getInstance(); + log.info("Executing on Raspberry PI"); + getPinList(); +// FIXME - uncomment this +// log.info("Initiating i2c"); +// I2CFactory.getInstance(Integer.parseInt(bus)); +// log.info("i2c initiated on bus {}", bus); +// addTask(1000, "scan"); +// +// log.info("read task initialized"); +// addTask(1000, "read"); + + // TODO - config which starts all pins in input or output mode + } catch (IOException e) { log.error("i2c initiation failed", e); + } catch (Exception e) { + // an error in the constructor won't get broadcast - so we need Runtime to + // do it + Runtime.getInstance().error("raspi service requires arm %s is not arm - %s", getName(), e.getMessage()); + log.error("RasPi init failed", e); + wrongPlatformError = "The RasPi service requires raspberry pi hardware"; + broadcastState(); } + } - public void testGPIOOutput() { - GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_02, PinMode.DIGITAL_INPUT, PinPullResistance.PULL_DOWN); - log.info("Pin: {}", pin); + public void test() { + // Create GPIO controller instance + GpioController gpio = GpioFactory.getInstance(); + + // Provision GPIO pin 0 as a digital input/output pin +// GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( +// RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT, PullUpResistance); + GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( + RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT); + + // Set the pin mode to output + pin.setMode(PinMode.DIGITAL_OUTPUT); + + // Write a value of 1 (HIGH) to GPIO pin 0 + pin.high(); + + // Delay for 2 seconds + sleep(2000); + + // Write a value of 0 (LOW) to GPIO pin 0 + pin.low(); + + // Delay for 2 seconds + sleep(2000); + + // Set the pin mode to input + pin.setMode(PinMode.DIGITAL_INPUT); + + // Add a listener to monitor pin state changes + pin.addListener((GpioPinListenerDigital) (GpioPinDigitalStateChangeEvent event) -> { + System.out.println("Pin state changed to: " + event.getState()); + }); + + // Shutdown GPIO controller and release resources + // gpio.shutdown(); } public void testPWM() { @@ -466,116 +768,26 @@ public void testPWM() { } } } catch (Exception e) { - + error(e); } } @Override - public void write(int address, int value) { - - PinDefinition pinDef = addressIndex.get(address); - pinMode(address, Arduino.OUTPUT); - digitalWrite(address, value); - // cache value - pinDef.setValue(value); - } - - public static void main(String[] args) { - LoggingFactory.init("info"); - - /* - * RasPi.displayString(1, 70, "1"); - * - * RasPi.displayString(1, 70, "abcd"); - * - * RasPi.displayString(1, 70, "1234"); - * - * - * //RasPi raspi = new RasPi("raspi"); - */ - - // raspi.writeDisplay(busAddress, deviceAddress, data) - - int i = 0; - - Runtime.start("servo01", "Servo"); - Runtime.start("ada16", "Adafruit16CServoDriver"); - Runtime.start(String.format("rasPi%d", i), "RasPi"); - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - - } - - @Override - public void reset() { - // TODO Auto-generated method stub - // reset pins/i2c devices/gpio pins - } - - @Override - public BoardInfo getBoardInfo() { - RaspiPin.allPins(); - // FIXME - this needs more work .. BoardInfo needs to be an interface where - // RasPiInfo is derived - return null; - } - - @Override - public List getBoardTypes() { - // TODO Auto-generated method stub - // FIXME - this need work - return null; - } - - // - add more pin mappings if desired ... - @Override - public Integer getAddress(String pin) { - return Integer.parseInt(pin); - } - - public void scan() { - scan(null); - } - - public void scan(Integer busNumber) { - - if (busNumber == null) { - busNumber = Integer.parseInt(bus); + public void write(String pin, int value) { + log.info("write {} {}", pin, value); + if (!pinIndex.containsKey(pin)) { + error("Pin %s not found", pin); + return; } - try { - - I2CBus bus = I2CFactory.getInstance(busNumber); + PinDefinition pinDef = pinIndex.get(pin); + pinMode(pin, "OUTPUT"); - validAddresses = new HashMap<>(); - - if (!validAddresses.containsKey(busNumber)) { - validAddresses.put(busNumber, new HashSet<>()); - } - - Set addresses = validAddresses.get(busNumber); - - for (int i = 1; i < 128; i++) { - try { - I2CDevice device = bus.getDevice(i); - device.write((byte) 0); - addresses.add(Integer.toHexString(i)); - } catch (Exception ignore) { - } - } - - log.info("scanning bus {} found: ---", busNumber); - for (String a : addresses) { - log.info("address: " + a); - } - log.info("----------"); - } catch (Exception e) { - error("cannot access i2c bus %d", busNumber); - log.error("scan threw", e); - } + GpioPinDigitalMultipurpose gpio = getGPIO(pin); + gpio.setState(value == 0 ? PinState.LOW : PinState.HIGH); + pinDef.setValue(value); - broadcastState(); + invoke("publishPinDefinition", pinDef); } } diff --git a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java index ef0c4769db..6e50cb496d 100644 --- a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java +++ b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java @@ -1,5 +1,10 @@ package org.myrobotlab.service.config; public class RasPiConfig extends ServiceConfig { - + /** + * reading poll rate for all enabled GPIO pins + * this "should" not be an int but a float, but at this time + * its better to follow the PinDefinition pollRateHz type + */ + public int pollRateHz = 1; } diff --git a/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js b/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js index ea81d00671..a25240dbae 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RasPiGui.js @@ -15,13 +15,18 @@ angular.module('mrlapp.service.RasPiGui', []).controller('RasPiGuiCtrl', ['$scop _self.updateState(data) $scope.$apply() break - case 'XXXonPinDefinition': + case 'onPinDefinition': $scope.service.pinIndex[data.pin] = data - $scope.service.addressIndex[data.address] = data + $scope.$apply() + break + case 'onPinArray': + for (const pd of data){ + $scope.service.pinIndex[pd.pin].value = pd.value + } $scope.$apply() break default: - $log.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) break } } @@ -31,16 +36,28 @@ angular.module('mrlapp.service.RasPiGui', []).controller('RasPiGuiCtrl', ['$scop } $scope.write = function(pinDef){ - msg.send('write', pinDef.address, pinDef.valueDisplay?1:0) + msg.send('write', pinDef.pin, pinDef.valueDisplay?1:0) } - $scope.readWrite = function(pinDef) { + $scope.pinMode = function(pinDef) { console.info(pinDef) // FIXME - standardize interface with Arduino :( - msg.send('pinMode', pinDef.pin, pinDef.readWrite?1:0) + msg.send('pinMode', pinDef.pin, pinDef.mode) } msg.subscribe('publishPinDefinition') + msg.subscribe('publishPinArray') msg.subscribe(this) } ]) +.filter('toArray', function() { + return function(obj) { + if (obj instanceof Object) { + return Object.keys(obj).map(function(key) { + return obj[key]; + }); + } else { + return obj; + } + }; + }); \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html index 9e4b0bb92f..d8ecb4b467 100644 --- a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html @@ -1,4 +1,8 @@
+
+

+ {{service.wrongPlatformError}} +

@@ -24,6 +35,7 @@
@@ -8,12 +12,19 @@
If you will be using I2C GPIO pins make sure you have enabled i2c with -
sudo raspiconfig 

-

-

-
sudo apt-get install -y i2c-tools
+
sudo raspi-config
+
+ +
+
+ +
+
+
sudo apt-get install -y i2c-tools
More recent rasbian distributions require building and installing this library https://github.com/WiringPi/WiringPi +
+ For information regarding Wiring pin numbering vs BCM visit : https://pinout.xyz/pinout/wiringpi
+ + bus {{index}} + {{index}} {{pin}}
@@ -50,17 +63,22 @@

-
- {{ pinDef.pin }} - +
+ {{ pinDef.pin }} + + + + + + {{pinDef.value}} + + Rx Tx - Pwm + Pwm Sda Scl - - - {{pinDef.value}} +
@@ -73,7 +91,9 @@ - + +
+ courtesy https://pinout.xyz
From dad01bc58d05ddf89c560b3577f9c4afb2dc9ddd Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 17 Jul 2023 06:30:19 -0700 Subject: [PATCH 004/232] forgot pindefinition --- .../org/myrobotlab/service/interfaces/PinDefinition.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java b/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java index 41658ef114..bdbeb9383c 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java +++ b/src/main/java/org/myrobotlab/service/interfaces/PinDefinition.java @@ -7,7 +7,7 @@ public class PinDefinition implements Serializable { private static final long serialVersionUID = 1L; /** - * label or name of the pin e.g. P0 D1 D2 etc... + * label or name of the pin e.g. P0, A5, D1, D2, GPIO 2, etc... */ String pin; @@ -25,6 +25,8 @@ public class PinDefinition implements Serializable { * pin mode INPUT or OUTPUT, other... */ String mode; + + public String serviceName; /** * statistics @@ -90,8 +92,13 @@ public void setScl(boolean isScl) { * rate in Hz for which the pin will be polled 0 == no rate imposed */ int pollRateHz = 0; + + public PinDefinition() { + } + public PinDefinition(String serviceName, int address, String pin) { + this.serviceName = serviceName; this.address = address; this.pin = pin; } From 4658b205798e11c3f2199466049fe8d42c1c5470 Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 17 Jul 2023 06:50:34 -0700 Subject: [PATCH 005/232] fixed legacy write and pinmode --- src/main/java/org/myrobotlab/service/RasPi.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index fd57408e16..367ce104e1 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -790,4 +790,14 @@ public void write(String pin, int value) { invoke("publishPinDefinition", pinDef); } + @Override + public void pinMode(int address, String mode) { + pinMode(String.format("%d", address), mode); + } + + @Override + public void write(int address, int value) { + write(String.format("%d", address), value); + } + } From 44401679e1f7481a4b779e6a9134ea43d7b5bf5d Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 17 Jul 2023 19:17:14 -0700 Subject: [PATCH 006/232] in progress --- .../org/myrobotlab/arduino/BoardInfo.java | 39 ++++++--- .../java/org/myrobotlab/service/Cron.java | 12 ++- .../java/org/myrobotlab/service/RasPi.java | 80 +++++++++++-------- 3 files changed, 88 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/myrobotlab/arduino/BoardInfo.java b/src/main/java/org/myrobotlab/arduino/BoardInfo.java index ebf9af74fe..517165b5cd 100644 --- a/src/main/java/org/myrobotlab/arduino/BoardInfo.java +++ b/src/main/java/org/myrobotlab/arduino/BoardInfo.java @@ -16,17 +16,38 @@ public class BoardInfo implements Serializable { private static final long serialVersionUID = 1L; transient public final static Logger log = LoggerFactory.getLogger(BoardInfo.class); - Integer version; - Integer boardTypeId; - Integer microsPerLoop; - Integer sram; - Integer activePins; - DeviceSummary[] deviceSummary; // deviceList with types + /** + * version of firmware + */ + public Integer version; + + /** + * id of board type - FIXME change to string + */ + public Integer boardTypeId; + + /** + * Number of microseconds arduino uses to pass through a + * control loop in MrlComm - very Arduino/MrlComm specific + * FIXME - make generalized BoardType that can report useful information + * from any type of board with pins + */ + public Integer microsPerLoop; + + /** + * + */ + public Integer sram; + public Integer activePins; + public DeviceSummary[] deviceSummary; // deviceList with types - String boardTypeName; + public String boardTypeName; - long heartbeatMs; - long receiveTs; + public long heartbeatMs; + public long receiveTs; + + public BoardInfo() { + } public BoardInfo(Integer version, Integer boardTypeId, String boardTypeName, Integer microsPerLoop, Integer sram, Integer activePins, DeviceSummary[] deviceSummary, long boardInfoRequestTs) { diff --git a/src/main/java/org/myrobotlab/service/Cron.java b/src/main/java/org/myrobotlab/service/Cron.java index b25f44bdcc..45346cd091 100644 --- a/src/main/java/org/myrobotlab/service/Cron.java +++ b/src/main/java/org/myrobotlab/service/Cron.java @@ -48,6 +48,9 @@ public static class Task implements Serializable, Runnable { */ public String method; + /** + * reference to service + */ transient Cron cron; /** @@ -73,8 +76,12 @@ public Task(Cron cron, String id, String cronPattern, String name, String method @Override public void run() { - log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data); - cron.send(name, method, data); + if (cron != null) { + log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data); + cron.send(name, method, data); + } else { + log.error("cron service is null"); + } } @Override @@ -227,6 +234,7 @@ public void start() { * stop the schedular ad all associated tasks */ public void stop() { + removeAllTasks(); if (scheduler.isStarted()) { scheduler.stop(); } diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index 367ce104e1..7d46d774cc 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -56,7 +56,7 @@ public static class I2CDeviceMap { transient public I2CDevice device; public int deviceHandle; public String serviceName; - + public String toString() { return String.format("bus: %d deviceHandle: %d service: %s", bus.getBusNumber(), deviceHandle, serviceName); } @@ -299,10 +299,29 @@ public Set getAttached() { @Override public BoardInfo getBoardInfo() { - RaspiPin.allPins(); - // FIXME - this needs more work .. BoardInfo needs to be an interface where - // RasPiInfo is derived - return null; + BoardInfo boardInfo = new BoardInfo(); + + try { + + // Get the board revision + String revision = SystemInfo.getRevision(); + log.info("Board Revision: " + revision); + + // Get the board type + boardInfo.boardTypeName = SystemInfo.getModelName(); + log.info("Board Model: " + boardInfo.boardTypeName); + + // Get the board's memory information + log.info("Memory Info: " + SystemInfo.getMemoryTotal()); + + // Get the board's operating system info + log.info("OS Name: " + SystemInfo.getOsName()); + + } catch (Exception e) { + error(e); + } + + return boardInfo; } @Override @@ -398,7 +417,7 @@ public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent // display pin state on console log.info(" --> GPIO PIN STATE CHANGE: {} = {}", event.getPin(), event.getState()); PinDefinition pindef = pinIndex.get(wiringToBcm.get(event.getPin().getName())); - if (pindef == null){ + if (pindef == null) { log.error("pindef is null for pin {}", event.getPin().getName()); } else { pindef.setValue(event.getState().getValue()); @@ -481,16 +500,16 @@ public int i2cWriteRead(I2CControl control, int busAddress, int deviceAddress, b * @param mode * INPUT = 0x0. Output = 0x1. */ - public void pinMode(String pin, String mode) { + public void pinMode(String pin, String mode) { log.info("pinMode {}, mode {}", pin, mode); - + if (mode == null) { error("Pin mode cannot be null"); return; } mode = mode.trim().toUpperCase(); - + if (!pinIndex.containsKey(pin)) { error("Pin %s not found", pin); return; @@ -508,10 +527,10 @@ public void pinMode(String pin, String mode) { } else { error("mode %s is not valid", mode); } - log.info("pinDef {}",pinDef); + log.info("pinDef {}", pinDef); invoke("publishPinDefinition", pinDef); } - + @Override public PinData publishPin(PinData pinData) { // TODO Make sure this method is invoked when a pin value interrupt is @@ -537,23 +556,23 @@ public void read() { pinArray.add(pd); } } - + if (pinArray.size() > 0) { PinData[] array = pinArray.toArray(new PinData[0]); - invoke("publishPinArray", new Object[]{array}); + invoke("publishPinArray", new Object[] { array }); } } - + @Override public int read(String pin) { - + if (!pinIndex.containsKey(pin)) { error("Pin %s not found", pin); return -1; } PinDefinition pindef = pinIndex.get(pin); GpioPinDigitalMultipurpose gpioPin = getGPIO(pin); - if (!gpioPin.isMode(PinMode.DIGITAL_INPUT)){ + if (!gpioPin.isMode(PinMode.DIGITAL_INPUT)) { pinMode(pin, "INPUT"); } if (gpioPin.isLow()) { @@ -564,7 +583,7 @@ public int read(String pin) { return 1; } } - + @Override public void reset() { // TODO Auto-generated method stub @@ -683,14 +702,14 @@ public void startService() { gpio = GpioFactory.getInstance(); log.info("Executing on Raspberry PI"); getPinList(); -// FIXME - uncomment this -// log.info("Initiating i2c"); -// I2CFactory.getInstance(Integer.parseInt(bus)); -// log.info("i2c initiated on bus {}", bus); -// addTask(1000, "scan"); -// -// log.info("read task initialized"); -// addTask(1000, "read"); + // FIXME - uncomment this + // log.info("Initiating i2c"); + // I2CFactory.getInstance(Integer.parseInt(bus)); + // log.info("i2c initiated on bus {}", bus); + // addTask(1000, "scan"); + // + // log.info("read task initialized"); + // addTask(1000, "read"); // TODO - config which starts all pins in input or output mode @@ -707,15 +726,12 @@ public void startService() { } + // FIXME - remove public void test() { // Create GPIO controller instance GpioController gpio = GpioFactory.getInstance(); - // Provision GPIO pin 0 as a digital input/output pin -// GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( -// RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT, PullUpResistance); - GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( - RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT); + GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT); // Set the pin mode to output pin.setMode(PinMode.DIGITAL_OUTPUT); @@ -737,7 +753,7 @@ public void test() { // Add a listener to monitor pin state changes pin.addListener((GpioPinListenerDigital) (GpioPinDigitalStateChangeEvent event) -> { - System.out.println("Pin state changed to: " + event.getState()); + log.info("Pin state changed to: " + event.getState()); }); // Shutdown GPIO controller and release resources @@ -797,7 +813,7 @@ public void pinMode(int address, String mode) { @Override public void write(int address, int value) { - write(String.format("%d", address), value); + write(String.format("%d", address), value); } } From 4443dc0de1038597f86f1fd181238f36798c0e67 Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 17 Jul 2023 19:19:25 -0700 Subject: [PATCH 007/232] oscope fixes --- .../resource/WebGui/app/widget/oscope.html | 17 +- .../resource/WebGui/app/widget/oscope.js | 323 +++++++++--------- 2 files changed, 179 insertions(+), 161 deletions(-) diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.html b/src/main/resources/resource/WebGui/app/widget/oscope.html index c25c00c834..7047c2654a 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.html +++ b/src/main/resources/resource/WebGui/app/widget/oscope.html @@ -1,25 +1,30 @@
-
+
- - - +
+
+
+
+
+
+
-
- +
+
diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.js b/src/main/resources/resource/WebGui/app/widget/oscope.js index 66b8271af3..d95b9f9098 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.js +++ b/src/main/resources/resource/WebGui/app/widget/oscope.js @@ -28,25 +28,25 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { }, // scope: true, link: function(scope, element) { - var _self = this; - var name = scope.serviceName; - var service = mrl.getService(name); - var mode = 'read'; + var _self = this + var name = scope.serviceName + var service = mrl.getService(name) + var mode = 'read' // 'read' || 'write' - var width = 800; - var height = 100; - var margin = 10; - var minY = margin; - var maxY = height - margin; - var scaleX = 1; - var scaleY = 1; - scope.readWrite = 'read'; + var width = 800 + var height = 100 + var margin = 10 + var minY = margin + var maxY = height - margin + var scaleX = 1 + var scaleY = 1 + scope.readWrite = 'read' // button toggle read/write - // scope.blah = {}; - // scope.blah.display = false; + // scope.blah = {} + // scope.blah.display = false + scope.pinIndex = service.pinIndex; - // scope.addressIndex = service.addressIndex; - var x = 0; + var x = 0 var gradient = tinygradient([{ h: 0, s: 0.4, @@ -57,44 +57,47 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { s: 0.4, v: 1, a: 1 - }]); - scope.oscope = {}; - scope.oscope.traces = {}; - scope.oscope.writeStates = {}; + }]) + scope.oscope = {} + scope.oscope.traces = {} + scope.oscope.writeStates = {} // display update interfaces // defintion stage var setTraceButtons = function(pinIndex) { if (pinIndex == null) { - return; + return } + + scope.addressIndex = mrl.getService(name).addressIndex + var size = Object.keys(pinIndex).length if (size && size > 0) { - scope.pinIndex = pinIndex; - var colorsHsv = gradient.hsv(size); + scope.pinIndex = pinIndex + var colorsHsv = gradient.hsv(size) // pass over pinIndex add display data - for (var key in pinIndex) { - if (!pinIndex.hasOwnProperty(key)) { - continue; + for (var pin in pinIndex) { + if (!pinIndex.hasOwnProperty(pin)) { + continue } - scope.oscope.traces[key] = {}; - var trace = scope.oscope.traces[key]; - var pinDef = pinIndex[key]; + scope.oscope.traces[pin] = {} + var trace = scope.oscope.traces[pin] + var pinDef = pinIndex[pin] // adding style - var color = colorsHsv[pinDef.address]; + var color = colorsHsv[pinDef.address] trace.readStyle = { 'background-color': color.toHexString() - }; + } trace.writeStyle = { 'background-color': '#eee' - }; - trace.color = color; - trace.state = false; + } + trace.color = color + trace.state = false // off - trace.posX = 0; - trace.posY = 0; - trace.count = 0; - trace.colorHexString = color.toHexString(); + trace.posX = 0 + trace.posY = 0 + trace.count = 0 + trace.colorHexString = color.toHexString() trace.stats = { min: 0, max: 1, @@ -106,213 +109,223 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { } // FIXME this should be _self.onMsg = function(inMsg) this.onMsg = function(inMsg) { - //console.log('CALLBACK - ' + msg.method); + //console.log('CALLBACK - ' + msg.method) switch (inMsg.method) { case 'onState': // backend update - setTraceButtons(inMsg.data[0].pinIndex); - scope.$apply(); - break; + setTraceButtons(inMsg.data[0].pinIndex) + scope.$apply() + break case 'onPinArray': - x++; - pinArray = inMsg.data[0]; + x++ + pinArray = inMsg.data[0] for (i = 0; i < pinArray.length; ++i) { // get pin data & definition - pinData = pinArray[i]; - pinDef = scope.pinIndex[pinData.pin]; + pinData = pinArray[i] + pinDef = scope.pinIndex[pinData.pin] // get correct screen and references - var screen = document.getElementById('oscope-pin-' + pinData.pin); + var screen = document.getElementById(scope.serviceName + '-oscope-pin-' + pinData.pin) var ctx = screen.getContext('2d'); - var trace = scope.oscope.traces[pinData.pin]; - var stats = trace.stats; + var trace = scope.oscope.traces[pinData.pin] + var stats = trace.stats // TODO - sample rate Hz - trace.stats.totalSample++; - trace.stats.totalValue += pinData.value; + trace.stats.totalSample++ + trace.stats.totalValue += pinData.value if (pinData.value < trace.stats.min) { - trace.stats.min = pinData.value; + trace.stats.min = pinData.value } if (pinData.value > trace.stats.max) { - trace.stats.max = pinData.value; + trace.stats.max = pinData.value } - var maxX = trace.stats.max; - var minX = trace.stats.min; - var c = minY + ((pinData.value - minX) * (maxY - minY)) / (maxX - minX); - var y = height - c; - ctx.beginPath(); + var maxX = trace.stats.max + var minX = trace.stats.min + var c = minY + ((pinData.value - minX) * (maxY - minY)) / (maxX - minX) + var y = height - c + ctx.beginPath() // from - ctx.moveTo(trace.posX, trace.posY); + ctx.moveTo(trace.posX, trace.posY) // to - ctx.lineTo(x, y); + ctx.lineTo(x, y) // save current values - trace.posX = x; - trace.posY = y; + trace.posX = x + trace.posY = y // color - ctx.strokeStyle = trace.colorHexString; + ctx.strokeStyle = trace.colorHexString // blank screen // TODO - continuous pan would be better - ctx.stroke(); + ctx.stroke() // blank screen if trace reaches end if (x > width) { - trace.state = true; - scope.highlight(trace, true); - //scope.toggleReadButton(pinDef); - ctx.font = "10px Aria"; - ctx.rect(0, 0, width, height); - ctx.fillStyle = "black"; - ctx.fill(); - var highlight = trace.color.getOriginalInput(); - highlight.s = "90%"; - var newColor = tinycolor(highlight); - ctx.fillStyle = trace.colorHexString; + trace.state = true + scope.highlight(trace, true) + //scope.toggleReadButton(pinDef) + ctx.font = "10px Aria" + ctx.rect(0, 0, width, height) + ctx.fillStyle = "black" + ctx.fill() + var highlight = trace.color.getOriginalInput() + highlight.s = "90%" + var newColor = tinycolor(highlight) + ctx.fillStyle = trace.colorHexString // TODO - highlight saturtion of text - ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY); - ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2); - ctx.fillText('MIN ' + stats.min, 10, maxY); - trace.posX = 0; + ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY) + ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2) + ctx.fillText('MIN ' + stats.min, 10, maxY) + trace.posX = 0 } // draw it - ctx.closePath(); + ctx.closePath() } // for each pin if (x > width) { - x = 0; + x = 0 } - break; + break default: // since we subscribed to "All" of Arduino's methods - most will escape here // no reason to put an error .. however, it would be better to "Only" susbscribe to the ones // we want - // console.log("ERROR - unhandled method " + inMsg.method); - break; + // console.log("ERROR - unhandled method " + inMsg.method) + break } } - ; + scope.toggleReadWrite = function() { - scope.readWrite = (scope.readWrite == 'write') ? 'read' : 'write'; + scope.readWrite = (scope.readWrite == 'write') ? 'read' : 'write' } - ; + scope.clearScreen = function(pinArray) { for (i = 0; i < pinArray.length; ++i) { - pinData = pinArray[i]; - pinDef = scope.pinIndex[pinData.pin]; - _self.ctx = screen.getContext('2d'); - // ctx.scale(1, -1); // flip y around for cartesian - bad idea :P - // width = screen.width; - //height = screen.height; - _self.ctx.rect(0, 0, width, height); - _self.ctx.fillStyle = "black"; - _self.ctx.fill(); - _self.ctx.fillStyle = "white"; - stats = pinDef.stats; - _self.ctx.fillText(pinDef.name + (' AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11) + ' MIN ' + stats.min + ' MAX ' + stats.max, 10, 18); + pinData = pinArray[i] + pinDef = scope.pinIndex[pinData.pin] + _self.ctx = screen.getContext('2d') + // ctx.scale(1, -1) // flip y around for cartesian - bad idea :P + // width = screen.width + //height = screen.height + _self.ctx.rect(0, 0, width, height) + _self.ctx.fillStyle = "black" + _self.ctx.fill() + _self.ctx.fillStyle = "white" + stats = pinDef.stats + _self.ctx.fillText(pinDef.name + (' AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11) + ' MIN ' + stats.min + ' MAX ' + stats.max, 10, 18) } } scope.zoomIn = function() { - scaleX += 1; - scaleY += 1; - _self.ctx.scale(scaleX, scaleY); + scaleX += 1 + scaleY += 1 + _self.ctx.scale(scaleX, scaleY) } - ; + // RENAME eanbleTrace - FIXME read values vs write values | ALL values from service not from ui !! - ui only sends commands scope.activateTrace = function(pinDef) { - var trace = scope.oscope.traces[pinDef.pin]; + var trace = scope.oscope.traces[pinDef.pin] if (trace.state) { - toggleReadButton(trace); - mrl.sendTo(name, 'disablePin', pinDef.pin); - trace.state = false; + toggleReadButton(trace) + mrl.sendTo(name, 'disablePin', pinDef.pin) + trace.state = false } else { - toggleReadButton(trace); - mrl.sendTo(name, 'enablePin', pinDef.pin); - trace.state = true; + toggleReadButton(trace) + // mrl.sendTo(name, 'enablePin', pinDef.pin) + mrl.sendTo(name, 'enablePin', pinDef.pin, 1) + trace.state = true } } - ; + scope.reset = function() { - mrl.sendTo(name, 'disablePins'); + mrl.sendTo(name, 'disablePins') } - ; + scope.write = function(pinDef) { - scope.toggleWriteButton(trace); - mrl.sendTo(name, 'digitalWrite', pinDef.pin, 1); - // trace.state = true; + scope.toggleWriteButton(trace) + mrl.sendTo(name, 'digitalWrite', pinDef.pin, 1) + // trace.state = true /* 3 states READ/ENABLE | DIGITALWRITE | ANALOGWRITE if (pinDef.pinName.charAt(0) == 'A') { - _self.toggleWriteButton(trace); - mrl.sendTo(name, 'analogWrite', 1); - trace.state = false; + _self.toggleWriteButton(trace) + mrl.sendTo(name, 'analogWrite', 1) + trace.state = false } else { - _self.toggleWriteButton(trace); - mrl.sendTo(name, 'digitalWrite', pinDef.address); - trace.state = true; + _self.toggleWriteButton(trace) + mrl.sendTo(name, 'digitalWrite', pinDef.address) + trace.state = true } */ } - ; + scope.reset = function() { - mrl.sendTo(name, 'disablePins'); + mrl.sendTo(name, 'disablePins') } - ; + var toggleReadButton = function(trace) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (trace.state) { - scope.highlight(trace, false); + scope.highlight(trace, false) } else { - scope.highlight(trace, true); + scope.highlight(trace, true) } - }; + } scope.highlight = function(trace, on) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (!on) { - // scope.blah.display = false; + // scope.blah.display = false // on to off - highlight.s = "40%"; - var newColor = color = tinycolor(highlight); + highlight.s = "40%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } else { - // scope.blah.display = true; + // scope.blah.display = true // off to on - highlight.s = "90%"; - var newColor = color = tinycolor(highlight); + highlight.s = "90%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } } - ; + scope.toggleWriteButton = function(pinDef) { - var highlight = trace.color.getOriginalInput(); + var highlight = trace.color.getOriginalInput() if (trace.state) { - // scope.blah.display = false; + // scope.blah.display = false // on to off - highlight.s = "40%"; - var newColor = color = tinycolor(highlight); + highlight.s = "40%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } else { - // scope.blah.display = true; + // scope.blah.display = true // off to on - highlight.s = "90%"; - var newColor = color = tinycolor(highlight); + highlight.s = "90%" + var newColor = color = tinycolor(highlight) trace.readStyle = { 'background-color': newColor.toHexString() - }; + } } } - ; + // FIXME FIXME FIXME ->> THIS SHOULD WORK subscribeToServiceMethod <- but doesnt - mrl.subscribeToService(_self.onMsg, name); + mrl.subscribeToService(_self.onMsg, name) // this siphons off a single subscribe to the webgui // so it will be broadcasted back to angular - mrl.subscribe(name, 'publishPinArray'); - mrl.subscribeToServiceMethod(_self.onMsg, name, 'publishPinArray'); + mrl.subscribe(name, 'publishPinArray') + mrl.subscribeToServiceMethod(_self.onMsg, name, 'publishPinArray') // initializing display data - setTraceButtons(service.pinIndex); + setTraceButtons(service.pinIndex) } - }; + } } -]); +]).filter('toArray', function() { + return function(obj) { + if (!angular.isObject(obj)) { + return obj; + } + return Object.keys(obj).map(function(key) { + return obj[key]; + }); + }; +}); From 370e128a78fe10cbfba7f5415c3c06307d1bc692 Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 17 Jul 2023 19:22:34 -0700 Subject: [PATCH 008/232] from grog branch --- .../java/org/myrobotlab/service/RasPi.java | 19 ++++++++++--------- .../service/config/RasPiConfig.java | 3 +++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index 7d46d774cc..481d6b2777 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -702,16 +702,14 @@ public void startService() { gpio = GpioFactory.getInstance(); log.info("Executing on Raspberry PI"); getPinList(); - // FIXME - uncomment this - // log.info("Initiating i2c"); - // I2CFactory.getInstance(Integer.parseInt(bus)); - // log.info("i2c initiated on bus {}", bus); - // addTask(1000, "scan"); - // - // log.info("read task initialized"); - // addTask(1000, "read"); - // TODO - config which starts all pins in input or output mode + log.info("Initiating i2c"); + I2CFactory.getInstance(Integer.parseInt(bus)); + log.info("i2c initiated on bus {}", bus); + addTask(1000, "scan"); + + log.info("read task initialized"); + addTask(1000, "read"); } catch (IOException e) { log.error("i2c initiation failed", e); @@ -731,6 +729,9 @@ public void test() { // Create GPIO controller instance GpioController gpio = GpioFactory.getInstance(); + // Provision GPIO pin 0 as a digital input/output pin + // GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin( + // RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT, PullUpResistance); GpioPinDigitalMultipurpose pin = gpio.provisionDigitalMultipurposePin(RaspiPin.GPIO_00, PinMode.DIGITAL_OUTPUT); // Set the pin mode to output diff --git a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java index 6e50cb496d..f10c2a1fc3 100644 --- a/src/main/java/org/myrobotlab/service/config/RasPiConfig.java +++ b/src/main/java/org/myrobotlab/service/config/RasPiConfig.java @@ -7,4 +7,7 @@ public class RasPiConfig extends ServiceConfig { * its better to follow the PinDefinition pollRateHz type */ public int pollRateHz = 1; + + // TODO - config which starts pins in a mode (read/write) and if write a value 0/1 + } From 22faefb0a68c93ded5fc01f114d00fa4ed106c4d Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 18 Jul 2023 14:36:21 -0700 Subject: [PATCH 009/232] oscope cron and raspi improvements --- README.md | 17 ++ .../java/org/myrobotlab/service/Arduino.java | 2 + .../java/org/myrobotlab/service/RasPi.java | 6 +- .../resource/WebGui/app/widget/oscope.html | 6 +- .../resource/WebGui/app/widget/oscope.js | 205 ++++++++---------- 5 files changed, 115 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index d5dc735b56..e48b5cb111 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,21 @@ resources: - ./src/main/resources/resource - ./src/main/resources type: WebGui +``` +```yml +!!org.myrobotlab.service.config.RuntimeConfig +enableCli: true +id: null +listeners: +locale: null +logLevel: info +peers: null +registry: +- runtime +- security +- webgui +- raspi +resource: src/main/resources/resource +type: Runtime +virtual: false ``` \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/Arduino.java b/src/main/java/org/myrobotlab/service/Arduino.java index 5fba97260d..3568fcb598 100644 --- a/src/main/java/org/myrobotlab/service/Arduino.java +++ b/src/main/java/org/myrobotlab/service/Arduino.java @@ -1495,6 +1495,8 @@ public synchronized void onConnect(String portName) { info("%s connected to %s", getName(), portName); // chained... invoke("publishConnect", portName); + + broadcastState(); } public void onCustomMsg(Integer ax, Integer ay, Integer az) { diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index 481d6b2777..866631de15 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -541,6 +541,10 @@ public PinData publishPin(PinData pinData) { return pinData; } + public Map> publishScan() { + return validI2CAddresses; + } + public void read() { log.debug("read task invoked"); List pinArray = new ArrayList<>(); @@ -638,7 +642,7 @@ public void scan(Integer busNumber) { log.error("scan threw", e); } - broadcastState(); + invoke("publishScan"); } // FIXME - return array diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.html b/src/main/resources/resource/WebGui/app/widget/oscope.html index 7047c2654a..90b4981eb6 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.html +++ b/src/main/resources/resource/WebGui/app/widget/oscope.html @@ -23,13 +23,15 @@
-
+
-
+
diff --git a/src/main/resources/resource/WebGui/app/widget/oscope.js b/src/main/resources/resource/WebGui/app/widget/oscope.js index d95b9f9098..f779cafa2d 100644 --- a/src/main/resources/resource/WebGui/app/widget/oscope.js +++ b/src/main/resources/resource/WebGui/app/widget/oscope.js @@ -31,8 +31,6 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { var _self = this var name = scope.serviceName var service = mrl.getService(name) - var mode = 'read' - // 'read' || 'write' var width = 800 var height = 100 var margin = 10 @@ -41,12 +39,8 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { var scaleX = 1 var scaleY = 1 scope.readWrite = 'read' - // button toggle read/write - // scope.blah = {} - // scope.blah.display = false - scope.pinIndex = service.pinIndex; - var x = 0 + // var x = 0 var gradient = tinygradient([{ h: 0, s: 0.4, @@ -61,50 +55,49 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { scope.oscope = {} scope.oscope.traces = {} scope.oscope.writeStates = {} - // display update interfaces - // defintion stage + var setTraceButtons = function(pinIndex) { - if (pinIndex == null) { + + if (Object.keys(scope.oscope.traces).length > 0) { return } - scope.addressIndex = mrl.getService(name).addressIndex - + // let pinIndex = service.pinIndex var size = Object.keys(pinIndex).length - if (size && size > 0) { - scope.pinIndex = pinIndex + if (size > 0) { var colorsHsv = gradient.hsv(size) - // pass over pinIndex add display data - for (var pin in pinIndex) { + + Object.keys(pinIndex).forEach(function(pin) { if (!pinIndex.hasOwnProperty(pin)) { - continue + return + } + var trace = { + readStyle: {}, + state: false, + posX: 0, + posY: 0, + x0: 0, + y0: 0, + x1: 0, + y1: 0, + count: 0, + stats: { + min: 0, + max: 1, + totalValue: 0, + totalSample: 1 + } } - scope.oscope.traces[pin] = {} - var trace = scope.oscope.traces[pin] var pinDef = pinIndex[pin] - - // adding style var color = colorsHsv[pinDef.address] - trace.readStyle = { - 'background-color': color.toHexString() - } - trace.writeStyle = { - 'background-color': '#eee' + if (!color) { + return } + trace.readStyle['background-color'] = color.toHexString() trace.color = color - trace.state = false - // off - trace.posX = 0 - trace.posY = 0 - trace.count = 0 trace.colorHexString = color.toHexString() - trace.stats = { - min: 0, - max: 1, - totalValue: 0, - totalSample: 1 - } - } + scope.oscope.traces[pin] = trace + }) } } // FIXME this should be _self.onMsg = function(inMsg) @@ -112,22 +105,49 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { //console.log('CALLBACK - ' + msg.method) switch (inMsg.method) { case 'onState': - // backend update + // backend update + scope.pinIndex = inMsg.data[0].pinIndex setTraceButtons(inMsg.data[0].pinIndex) scope.$apply() break case 'onPinArray': - x++ + // all pin traces are going to be traced at the same x position + // x++ pinArray = inMsg.data[0] for (i = 0; i < pinArray.length; ++i) { // get pin data & definition pinData = pinArray[i] pinDef = scope.pinIndex[pinData.pin] // get correct screen and references - var screen = document.getElementById(scope.serviceName + '-oscope-pin-' + pinData.pin) - var ctx = screen.getContext('2d'); - var trace = scope.oscope.traces[pinData.pin] - var stats = trace.stats + // change to LET !! + let screen = document.getElementById(scope.serviceName + '-oscope-pin-' + pinData.pin) + let ctx = screen.getContext('2d'); + let trace = scope.oscope.traces[pinData.pin] + let stats = trace.stats + + // blank screen if trace reaches end + if (trace.x1 > width || trace.x0 == 0) { + trace.state = true + scope.highlight(trace, true) + ctx.font = "10px Aria" + ctx.rect(0, 0, width, height) + ctx.fillStyle = "black" + ctx.fill() + var highlight = trace.color.getOriginalInput() + highlight.s = "90%" + var newColor = tinycolor(highlight) + ctx.fillStyle = trace.colorHexString + // TODO - highlight saturtion of text + ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY) + ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2) + ctx.fillText('MIN ' + stats.min, 10, maxY) + trace.x0 = 0 + trace.x1 = 0 + } + // draw it + + + // TODO - sample rate Hz trace.stats.totalSample++ trace.stats.totalValue += pinData.value @@ -142,44 +162,22 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { var c = minY + ((pinData.value - minX) * (maxY - minY)) / (maxX - minX) var y = height - c ctx.beginPath() - // from - ctx.moveTo(trace.posX, trace.posY) - // to - ctx.lineTo(x, y) + // move to last position... + ctx.moveTo(trace.x0, trace.y0) + trace.x1++ + trace.y1 = c + // draw line to x1,y1 + ctx.lineTo(trace.x1, trace.y1) // save current values - trace.posX = x - trace.posY = y + trace.x0 = trace.x1 + trace.y0 = trace.y1 // color ctx.strokeStyle = trace.colorHexString // blank screen // TODO - continuous pan would be better ctx.stroke() - // blank screen if trace reaches end - if (x > width) { - trace.state = true - scope.highlight(trace, true) - //scope.toggleReadButton(pinDef) - ctx.font = "10px Aria" - ctx.rect(0, 0, width, height) - ctx.fillStyle = "black" - ctx.fill() - var highlight = trace.color.getOriginalInput() - highlight.s = "90%" - var newColor = tinycolor(highlight) - ctx.fillStyle = trace.colorHexString - // TODO - highlight saturtion of text - ctx.fillText('MAX ' + stats.max + ' ' + pinDef.pin + ' ' + pinDef.address, 10, minY) - ctx.fillText(('AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11), 10, height / 2) - ctx.fillText('MIN ' + stats.min, 10, maxY) - trace.posX = 0 - } - // draw it ctx.closePath() } - // for each pin - if (x > width) { - x = 0 - } break default: // since we subscribed to "All" of Arduino's methods - most will escape here @@ -189,19 +187,12 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { break } } - - scope.toggleReadWrite = function() { - scope.readWrite = (scope.readWrite == 'write') ? 'read' : 'write' - } - + scope.clearScreen = function(pinArray) { - for (i = 0; i < pinArray.length; ++i) { + for (i = 0; i < scope.pinArray.length; ++i) { pinData = pinArray[i] pinDef = scope.pinIndex[pinData.pin] _self.ctx = screen.getContext('2d') - // ctx.scale(1, -1) // flip y around for cartesian - bad idea :P - // width = screen.width - //height = screen.height _self.ctx.rect(0, 0, width, height) _self.ctx.fillStyle = "black" _self.ctx.fill() @@ -210,12 +201,7 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { _self.ctx.fillText(pinDef.name + (' AVG ' + (stats.totalValue / stats.totalSample)).substring(0, 11) + ' MIN ' + stats.min + ' MAX ' + stats.max, 10, 18) } } - scope.zoomIn = function() { - scaleX += 1 - scaleY += 1 - _self.ctx.scale(scaleX, scaleY) - } - + // RENAME eanbleTrace - FIXME read values vs write values | ALL values from service not from ui !! - ui only sends commands scope.activateTrace = function(pinDef) { var trace = scope.oscope.traces[pinDef.pin] @@ -230,33 +216,15 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { trace.state = true } } - + scope.reset = function() { mrl.sendTo(name, 'disablePins') } - - scope.write = function(pinDef) { - scope.toggleWriteButton(trace) - mrl.sendTo(name, 'digitalWrite', pinDef.pin, 1) - // trace.state = true - /* 3 states READ/ENABLE | DIGITALWRITE | ANALOGWRITE - if (pinDef.pinName.charAt(0) == 'A') { - _self.toggleWriteButton(trace) - mrl.sendTo(name, 'analogWrite', 1) - trace.state = false - } else { - _self.toggleWriteButton(trace) - mrl.sendTo(name, 'digitalWrite', pinDef.address) - trace.state = true - } - */ - } - scope.reset = function() { mrl.sendTo(name, 'disablePins') } - + var toggleReadButton = function(trace) { var highlight = trace.color.getOriginalInput() if (trace.state) { @@ -285,7 +253,7 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { } } } - + scope.toggleWriteButton = function(pinDef) { var highlight = trace.color.getOriginalInput() if (trace.state) { @@ -306,7 +274,7 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { } } } - + // FIXME FIXME FIXME ->> THIS SHOULD WORK subscribeToServiceMethod <- but doesnt mrl.subscribeToService(_self.onMsg, name) // this siphons off a single subscribe to the webgui @@ -320,12 +288,13 @@ angular.module('mrlapp.service').directive('oscope', ['mrl', function(mrl) { } } ]).filter('toArray', function() { - return function(obj) { - if (!angular.isObject(obj)) { - return obj; + return function(obj) { + if (!angular.isObject(obj)) { + return obj; + } + return Object.keys(obj).map(function(key) { + return obj[key]; + }); } - return Object.keys(obj).map(function(key) { - return obj[key]; - }); - }; + ; }); From bac8ca92a69d096a70672c46c1131eea71cd170c Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 18 Jul 2023 19:34:34 -0700 Subject: [PATCH 010/232] ipscan image and info down to debug --- src/main/java/org/myrobotlab/service/RasPi.java | 6 +++--- src/main/resources/resource/RasPi/i2c-scan.png | Bin 0 -> 6920 bytes .../WebGui/app/service/views/RasPiGui.html | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/resource/RasPi/i2c-scan.png diff --git a/src/main/java/org/myrobotlab/service/RasPi.java b/src/main/java/org/myrobotlab/service/RasPi.java index 866631de15..5cdc18f0d8 100644 --- a/src/main/java/org/myrobotlab/service/RasPi.java +++ b/src/main/java/org/myrobotlab/service/RasPi.java @@ -339,7 +339,7 @@ public List getBoardTypes() { * @return */ private GpioPinDigitalMultipurpose getGPIO(String pin) { - log.info("getGPIO {}", pin); + log.debug("getGPIO {}", pin); if (!pinIndex.containsKey(pin)) { error("Pin %s not found", pin); return null; @@ -552,11 +552,11 @@ public void read() { for (String pin : pinIndex.keySet()) { PinDefinition pindef = pinIndex.get(pin); if (pindef.isEnabled()) { - log.info("pin {} enabled {}", pin, pindef.isEnabled()); + log.debug("pin {} enabled {}", pin, pindef.isEnabled()); int value = read(pin); pindef.setValue(value); PinData pd = new PinData(pin, value); - log.info("pin data {}", pd); + log.debug("pin data {}", pd); pinArray.add(pd); } } diff --git a/src/main/resources/resource/RasPi/i2c-scan.png b/src/main/resources/resource/RasPi/i2c-scan.png new file mode 100644 index 0000000000000000000000000000000000000000..4648fe46dc500f4247ecd6ee23adadd463ffc13c GIT binary patch literal 6920 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html index d8ecb4b467..27bfb2d312 100644 --- a/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/RasPiGui.html @@ -19,8 +19,10 @@



+
sudo apt-get install -y i2c-tools

+ +

-
sudo apt-get install -y i2c-tools
More recent rasbian distributions require building and installing this library https://github.com/WiringPi/WiringPi
From ea0ab741862a87e3bd65bdf653a9b24c624188ab Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 18 Jul 2023 19:42:12 -0700 Subject: [PATCH 011/232] adding .gitignore entries --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 803d7438d8..f8af3ea9dd 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ /lastRestart.py /.factorypath start.yml +config +src/main/resources/resource/InMoov2 +src/main/resources/resource/ProgramAB \ No newline at end of file From f1c691c2ca3bd6b6ea1e03530063c3a70f2699b7 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 20 Jul 2023 06:10:03 -0700 Subject: [PATCH 012/232] generated pom --- pom.xml | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) 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 + * + + + + + + From 993669c30136f82d16f9df8daca30363f6aa415f Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 15:36:14 -0700 Subject: [PATCH 013/232] rename --- .../java/org/myrobotlab/service/WebXr.java | 87 ------------------- .../service/config/WebXrConfig.java | 37 -------- .../myrobotlab/service/meta/WebXrMeta.java | 38 -------- 3 files changed, 162 deletions(-) delete mode 100644 src/main/java/org/myrobotlab/service/WebXr.java delete mode 100644 src/main/java/org/myrobotlab/service/config/WebXrConfig.java delete mode 100644 src/main/java/org/myrobotlab/service/meta/WebXrMeta.java diff --git a/src/main/java/org/myrobotlab/service/WebXr.java b/src/main/java/org/myrobotlab/service/WebXr.java deleted file mode 100644 index 87df997800..0000000000 --- a/src/main/java/org/myrobotlab/service/WebXr.java +++ /dev/null @@ -1,87 +0,0 @@ -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); - - return pose; - } - - // TODO publishQuaternion - - 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/WebXrConfig.java b/src/main/java/org/myrobotlab/service/config/WebXrConfig.java deleted file mode 100644 index c97615440b..0000000000 --- a/src/main/java/org/myrobotlab/service/config/WebXrConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.myrobotlab.service.config; - -import java.util.HashMap; -import java.util.Map; - -import org.myrobotlab.math.MapperSimple; - -public class WebXrConfig extends ServiceConfig { - - public Integer port = 8888; - public boolean autoStartBrowser = true; - - /** - * range and name mappings for orientation and position - * controller name | servo name | mapping - */ - public Map> mappings = new HashMap<>(); - - public WebXrConfig() { - - Map map = new HashMap<>(); - map.put("i01.head.rollNeck", new MapperSimple(-3.14, 3.14, -90, 270)); - mappings.put("head.orientation.roll", map); - - map = new HashMap<>(); - map.put("i01.head.rothead", new MapperSimple(-3.14, 3.14, -90, 270)); - mappings.put("head.orientation.yaw", map); - - map = new HashMap<>(); - map.put("i01.head.neck", new MapperSimple(-3.14, 3.14, -90, 270)); - mappings.put("head.orientation.pitch", map); - - } - -} - - diff --git a/src/main/java/org/myrobotlab/service/meta/WebXrMeta.java b/src/main/java/org/myrobotlab/service/meta/WebXrMeta.java deleted file mode 100644 index 46b0921214..0000000000 --- a/src/main/java/org/myrobotlab/service/meta/WebXrMeta.java +++ /dev/null @@ -1,38 +0,0 @@ -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 dependencies if necessary - // addDependency("com.twelvemonkeys.common", "common-lang", "3.1.1"); - - setAvailable(false); - - // add it to one or many categories - addCategory("remote","control"); - - // add a sponsor to this service - // the person who will do maintenance - // setSponsor("GroG"); - - } - -} From b7ddb43c364fa39da7b0e14583e620d7c2d7948c Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 15:37:20 -0700 Subject: [PATCH 014/232] incremental --- .../java/org/myrobotlab/service/Cron.java | 188 +++++++++++------- .../java/org/myrobotlab/service/Hd44780.java | 20 +- .../org/myrobotlab/service/ProgramAB.java | 2 +- .../java/org/myrobotlab/service/Vertx.java | 19 ++ .../java/org/myrobotlab/service/WebXR.java | 87 ++++++++ .../myrobotlab/service/config/CronConfig.java | 5 +- .../myrobotlab/service/meta/WebXRMeta.java | 33 +++ .../resource/WebGui/app/service/js/CronGui.js | 18 +- .../WebGui/app/service/views/CronGui.html | 17 +- 9 files changed, 300 insertions(+), 89 deletions(-) create mode 100644 src/main/java/org/myrobotlab/service/WebXR.java create mode 100644 src/main/java/org/myrobotlab/service/meta/WebXRMeta.java diff --git a/src/main/java/org/myrobotlab/service/Cron.java b/src/main/java/org/myrobotlab/service/Cron.java index a6826c9b25..e52e9fe2b5 100644 --- a/src/main/java/org/myrobotlab/service/Cron.java +++ b/src/main/java/org/myrobotlab/service/Cron.java @@ -1,8 +1,11 @@ package org.myrobotlab.service; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.UUID; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; @@ -10,6 +13,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.CronConfig; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import it.sauronsoftware.cron4j.Scheduler; @@ -24,10 +28,15 @@ * */ public class Cron extends Service { - + public static class Task implements Serializable, Runnable { private static final long serialVersionUID = 1L; + /** + * reference to service + */ + transient Cron cron; + /** * cron pattern for this task */ @@ -44,7 +53,7 @@ public static class Task implements Serializable, Runnable { transient public String hash; /** - * unique id for the user to use + * id for the user to use */ public String id; @@ -53,11 +62,6 @@ public static class Task implements Serializable, Runnable { */ public String method; - /** - * reference to service - */ - transient Cron cron; - /** * name of the target service */ @@ -81,12 +85,13 @@ public Task(Cron cron, String id, String cronPattern, String name, String method @Override public void run() { - if (cron != null) { log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data); cron.send(name, method, data); - } else { - log.error("cron service is null"); - } + cron.history.add(new TaskHistory(id, new Date())); + if (cron.history.size() > cron.HISTORY_SIZE) { + cron.history.remove(0); + } + cron.broadcastState(); } @Override @@ -94,16 +99,41 @@ public String toString() { return String.format("%s, %s, %s, %s", id, cronPattern, name, method); } } + + public static class TaskHistory { + public String id; + public Date processedTime; + + public TaskHistory(String id, Date now) { + this.id = id; + this.processedTime = now; + } + } public final static Logger log = LoggerFactory.getLogger(Cron.class); private static final long serialVersionUID = 1L; + /** + * history buffer of tasks that have been executed + */ + final protected List history = new ArrayList<>(); + + /** + * max size of history buffer + */ + final int HISTORY_SIZE = 30; + /** * the thing that translates all the cron pattern values and implements actual tasks */ transient private Scheduler scheduler = new Scheduler(); + /** + * map of tasks organized by id + */ + public Map tasks = new LinkedHashMap<>(); + public Cron(String n, String id) { super(n, id); } @@ -117,73 +147,89 @@ public Cron(String n, String id) { * @param method * @return */ - public String addNamedTask(String id, String cron, String serviceName, String method) { - return addNamedTask(id, cron, serviceName, method, (Object[]) null); + public String addTask(String id, String cron, String serviceName, String method) { + return addTask(id, cron, serviceName, method, (Object[]) null); } /** * Add a named task with parameters * * @param id - * @param cron + * @param cronPattern * @param serviceName * @param method * @param data * @return */ - public String addNamedTask(String id, String cron, String serviceName, String method, Object... data) { - CronConfig c = (CronConfig) config; - Task task = new Task(this, id, cron, serviceName, method, data); - task.id = id; - task.hash = scheduler.schedule(cron, task); - c.tasks.put(id, task); - broadcastState(); + public String addTask(String id, String cronPattern, String serviceName, String method, Object... data) { + Task task = new Task(this, id, cronPattern, serviceName, method, data); + addTask(task); return id; } + /** * * @param task * @return */ - public String addNamedTask(Task task) { - CronConfig c = (CronConfig) config; + public String addTask(Task task) { + if (tasks.containsKey(task.id)) { + log.info("descheduling prexisting task {} hash {}", task.id, task.hash); + scheduler.deschedule(task.id); + } + log.info("scheduling task {}", task.id); task.hash = scheduler.schedule(task.cronPattern, task); - c.tasks.put(task.id, task); + task.cron = this; + tasks.put(task.id, task); broadcastState(); return task.id; } - /** - * Add a task with out parameters, the name will be generated guid - * - * @param cron - * @param serviceName - * @param method - * @return - */ - public String addTask(String cron, String serviceName, String method) { - String id = UUID.randomUUID().toString(); - return addNamedTask(id, cron, serviceName, method, (Object[]) null); + @Override + public ServiceConfig apply(ServiceConfig c) { + // deschedule current tasks + removeAllTasks(); + + // add new tasks + CronConfig config = (CronConfig)c; + for (Task task : config.tasks) { + addTask(task); + } + return c; + } + + @Override + public ServiceConfig getConfig() { + CronConfig c = (CronConfig)config; + c.tasks = new ArrayList<>(); + for (Task task: tasks.values()) { + c.tasks.add(task); + } + return c; + } + + public Map getCronTasks() { + return tasks; } /** - * Add a task with parameters, the name will be generated guid - * - * @param cron - * @param serviceName - * @param method - * @param data + * get a task from id + * @param id * @return */ - public String addTask(String cron, String serviceName, String method, Object... data) { - String id = UUID.randomUUID().toString(); - return addNamedTask(id, cron, serviceName, method, data); + public Task getTask(String id) { + return tasks.get(id); } - public Map getCronTasks() { - CronConfig c = (CronConfig) config; - return c.tasks; + /** + * removes all the tasks without stopping the scheduler + */ + public void removeAllTasks() { + for (Task t : tasks.values()) { + scheduler.deschedule(t.hash); + } + tasks.clear(); } /** @@ -192,8 +238,7 @@ public Map getCronTasks() { * @return the removed task if it exists */ public Task removeTask(String id) { - CronConfig c = (CronConfig) config; - Task t = c.tasks.remove(id); + Task t = tasks.remove(id); if (t != null) { scheduler.deschedule(t.hash); } else { @@ -202,39 +247,22 @@ public Task removeTask(String id) { broadcastState(); return t; } - + /** - * removes all the tasks without stopping the scheduler + * start the schedular and all associated tasks */ - public void removeAllTasks() { - CronConfig c = (CronConfig) config; - for (Task t : c.tasks.values()) { - scheduler.deschedule(t.hash); + public void start() { + if (!scheduler.isStarted()) { + scheduler.start(); } - c.tasks.clear(); } - + @Override public void startService() { super.startService(); start(); } - - @Override - public void stopService() { - super.stopService(); - stop(); - } - - /** - * start the schedular and all associated tasks - */ - public void start() { - if (!scheduler.isStarted()) { - scheduler.start(); - } - } - + /** * stop the schedular ad all associated tasks */ @@ -244,7 +272,14 @@ public void stop() { scheduler.stop(); } } + + @Override + public void stopService() { + super.stopService(); + stop(); + } + public static void main(String[] args) { LoggingFactory.init(Level.INFO); @@ -262,9 +297,9 @@ public static void main(String[] args) { * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0); */ // every odd minute - String id = cron.addNamedTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1); + String id = cron.addTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1); // every event minute - String id2 = cron.addNamedTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0); + String id2 = cron.addTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0); // Runtime.createAndStart("webgui", "WebGui"); @@ -272,4 +307,5 @@ public static void main(String[] args) { Logging.logError(e); } } + } diff --git a/src/main/java/org/myrobotlab/service/Hd44780.java b/src/main/java/org/myrobotlab/service/Hd44780.java index da1750d5c2..d20adf6986 100644 --- a/src/main/java/org/myrobotlab/service/Hd44780.java +++ b/src/main/java/org/myrobotlab/service/Hd44780.java @@ -13,6 +13,7 @@ import org.myrobotlab.service.config.Hd44780Config; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; +import org.myrobotlab.service.interfaces.TextListener; import org.slf4j.Logger; /** @@ -24,7 +25,7 @@ * @author Moz4r modified by Ray Edgley. * */ -public class Hd44780 extends Service { +public class Hd44780 extends Service implements TextListener { public final static Logger log = LoggerFactory.getLogger(Hd44780.class); @@ -205,6 +206,16 @@ public void display(String string, int line) { } lcdWriteDataString(string); } + + /** + * display the text + * FIXME - should by default scroll if text is larger than the width of the hd + * @param text + */ + public void display(String text) { + // FIXME - lame, but going to default this way + display(text, 0); + } /** * Clear lcd and set to home. @@ -710,4 +721,11 @@ public ServiceConfig apply(ServiceConfig c) { } return c; } + + + @Override + public void onText(String text) throws Exception { + display(text); + } + } \ No newline at end of file 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 index 26258ddb5c..f87e9b9e4c 100644 --- a/src/main/java/org/myrobotlab/service/Vertx.java +++ b/src/main/java/org/myrobotlab/service/Vertx.java @@ -36,6 +36,9 @@ 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"); @@ -62,7 +65,23 @@ public void start() { 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(); 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..22328d1b39 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -0,0 +1,87 @@ +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); + + return pose; + } + + // TODO publishQuaternion + + 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/CronConfig.java b/src/main/java/org/myrobotlab/service/config/CronConfig.java index 5bb0c50ff5..974526c1f2 100644 --- a/src/main/java/org/myrobotlab/service/config/CronConfig.java +++ b/src/main/java/org/myrobotlab/service/config/CronConfig.java @@ -1,11 +1,12 @@ package org.myrobotlab.service.config; -import java.util.LinkedHashMap; +import java.util.ArrayList; +import java.util.List; import org.myrobotlab.service.Cron.Task; public class CronConfig extends ServiceConfig { - public LinkedHashMap tasks = new LinkedHashMap<>(); + public List tasks = new ArrayList<>(); } 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/resources/resource/WebGui/app/service/js/CronGui.js b/src/main/resources/resource/WebGui/app/service/js/CronGui.js index 80ce7a109b..7fcf13e0ff 100644 --- a/src/main/resources/resource/WebGui/app/service/js/CronGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/CronGui.js @@ -12,7 +12,7 @@ angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope' cronPattern: null, name: null, method: null, - data: null + data: null } // GOOD TEMPLATE TO FOLLOW @@ -34,20 +34,24 @@ angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope' } $scope.addNamedTask = function() { - if ($scope.parameters && $scope.parameters.length > 0){ + if ($scope.parameters && $scope.parameters.length > 0) { $scope.newTask.data = JSON.parse($scope.parameters) } else { $scope.newTask.data = null } - - msg.send('addNamedTask', $scope.newTask) + + msg.send('addTask', $scope.newTask) } - + $scope.removeTask = function(id) { msg.send('removeTask', id) } - msg.subscribe(this) } -]) +]).filter('epochToLocalDate', function() { + return function(epochTime) { + return new Date(epochTime).toLocaleString(); + } + ; +}); diff --git a/src/main/resources/resource/WebGui/app/service/views/CronGui.html b/src/main/resources/resource/WebGui/app/service/views/CronGui.html index 78f17783cb..c1f31bd3cd 100644 --- a/src/main/resources/resource/WebGui/app/service/views/CronGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/CronGui.html @@ -8,13 +8,13 @@

Cron Tab

- + - + @@ -52,6 +52,19 @@

Cron Tab

+ + + + + + + + + + + + +
nameid cron service method parameters
History
{{history.id}}{{ history.processedTime | epochToLocalDate }}

From 754b978718bcf8a336d5a27e1323f2b653660922 Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 15:42:51 -0700 Subject: [PATCH 015/232] Improved Cron and Cron history --- .../java/org/myrobotlab/service/Cron.java | 188 +++++++++++------- .../myrobotlab/service/config/CronConfig.java | 5 +- .../resource/WebGui/app/service/js/CronGui.js | 18 +- .../WebGui/app/service/views/CronGui.html | 17 +- 4 files changed, 141 insertions(+), 87 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/Cron.java b/src/main/java/org/myrobotlab/service/Cron.java index a6826c9b25..e52e9fe2b5 100644 --- a/src/main/java/org/myrobotlab/service/Cron.java +++ b/src/main/java/org/myrobotlab/service/Cron.java @@ -1,8 +1,11 @@ package org.myrobotlab.service; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.UUID; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; @@ -10,6 +13,7 @@ import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.CronConfig; +import org.myrobotlab.service.config.ServiceConfig; import org.slf4j.Logger; import it.sauronsoftware.cron4j.Scheduler; @@ -24,10 +28,15 @@ * */ public class Cron extends Service { - + public static class Task implements Serializable, Runnable { private static final long serialVersionUID = 1L; + /** + * reference to service + */ + transient Cron cron; + /** * cron pattern for this task */ @@ -44,7 +53,7 @@ public static class Task implements Serializable, Runnable { transient public String hash; /** - * unique id for the user to use + * id for the user to use */ public String id; @@ -53,11 +62,6 @@ public static class Task implements Serializable, Runnable { */ public String method; - /** - * reference to service - */ - transient Cron cron; - /** * name of the target service */ @@ -81,12 +85,13 @@ public Task(Cron cron, String id, String cronPattern, String name, String method @Override public void run() { - if (cron != null) { log.info("{} Cron firing message {}->{}.{}", cron.getName(), name, method, data); cron.send(name, method, data); - } else { - log.error("cron service is null"); - } + cron.history.add(new TaskHistory(id, new Date())); + if (cron.history.size() > cron.HISTORY_SIZE) { + cron.history.remove(0); + } + cron.broadcastState(); } @Override @@ -94,16 +99,41 @@ public String toString() { return String.format("%s, %s, %s, %s", id, cronPattern, name, method); } } + + public static class TaskHistory { + public String id; + public Date processedTime; + + public TaskHistory(String id, Date now) { + this.id = id; + this.processedTime = now; + } + } public final static Logger log = LoggerFactory.getLogger(Cron.class); private static final long serialVersionUID = 1L; + /** + * history buffer of tasks that have been executed + */ + final protected List history = new ArrayList<>(); + + /** + * max size of history buffer + */ + final int HISTORY_SIZE = 30; + /** * the thing that translates all the cron pattern values and implements actual tasks */ transient private Scheduler scheduler = new Scheduler(); + /** + * map of tasks organized by id + */ + public Map tasks = new LinkedHashMap<>(); + public Cron(String n, String id) { super(n, id); } @@ -117,73 +147,89 @@ public Cron(String n, String id) { * @param method * @return */ - public String addNamedTask(String id, String cron, String serviceName, String method) { - return addNamedTask(id, cron, serviceName, method, (Object[]) null); + public String addTask(String id, String cron, String serviceName, String method) { + return addTask(id, cron, serviceName, method, (Object[]) null); } /** * Add a named task with parameters * * @param id - * @param cron + * @param cronPattern * @param serviceName * @param method * @param data * @return */ - public String addNamedTask(String id, String cron, String serviceName, String method, Object... data) { - CronConfig c = (CronConfig) config; - Task task = new Task(this, id, cron, serviceName, method, data); - task.id = id; - task.hash = scheduler.schedule(cron, task); - c.tasks.put(id, task); - broadcastState(); + public String addTask(String id, String cronPattern, String serviceName, String method, Object... data) { + Task task = new Task(this, id, cronPattern, serviceName, method, data); + addTask(task); return id; } + /** * * @param task * @return */ - public String addNamedTask(Task task) { - CronConfig c = (CronConfig) config; + public String addTask(Task task) { + if (tasks.containsKey(task.id)) { + log.info("descheduling prexisting task {} hash {}", task.id, task.hash); + scheduler.deschedule(task.id); + } + log.info("scheduling task {}", task.id); task.hash = scheduler.schedule(task.cronPattern, task); - c.tasks.put(task.id, task); + task.cron = this; + tasks.put(task.id, task); broadcastState(); return task.id; } - /** - * Add a task with out parameters, the name will be generated guid - * - * @param cron - * @param serviceName - * @param method - * @return - */ - public String addTask(String cron, String serviceName, String method) { - String id = UUID.randomUUID().toString(); - return addNamedTask(id, cron, serviceName, method, (Object[]) null); + @Override + public ServiceConfig apply(ServiceConfig c) { + // deschedule current tasks + removeAllTasks(); + + // add new tasks + CronConfig config = (CronConfig)c; + for (Task task : config.tasks) { + addTask(task); + } + return c; + } + + @Override + public ServiceConfig getConfig() { + CronConfig c = (CronConfig)config; + c.tasks = new ArrayList<>(); + for (Task task: tasks.values()) { + c.tasks.add(task); + } + return c; + } + + public Map getCronTasks() { + return tasks; } /** - * Add a task with parameters, the name will be generated guid - * - * @param cron - * @param serviceName - * @param method - * @param data + * get a task from id + * @param id * @return */ - public String addTask(String cron, String serviceName, String method, Object... data) { - String id = UUID.randomUUID().toString(); - return addNamedTask(id, cron, serviceName, method, data); + public Task getTask(String id) { + return tasks.get(id); } - public Map getCronTasks() { - CronConfig c = (CronConfig) config; - return c.tasks; + /** + * removes all the tasks without stopping the scheduler + */ + public void removeAllTasks() { + for (Task t : tasks.values()) { + scheduler.deschedule(t.hash); + } + tasks.clear(); } /** @@ -192,8 +238,7 @@ public Map getCronTasks() { * @return the removed task if it exists */ public Task removeTask(String id) { - CronConfig c = (CronConfig) config; - Task t = c.tasks.remove(id); + Task t = tasks.remove(id); if (t != null) { scheduler.deschedule(t.hash); } else { @@ -202,39 +247,22 @@ public Task removeTask(String id) { broadcastState(); return t; } - + /** - * removes all the tasks without stopping the scheduler + * start the schedular and all associated tasks */ - public void removeAllTasks() { - CronConfig c = (CronConfig) config; - for (Task t : c.tasks.values()) { - scheduler.deschedule(t.hash); + public void start() { + if (!scheduler.isStarted()) { + scheduler.start(); } - c.tasks.clear(); } - + @Override public void startService() { super.startService(); start(); } - - @Override - public void stopService() { - super.stopService(); - stop(); - } - - /** - * start the schedular and all associated tasks - */ - public void start() { - if (!scheduler.isStarted()) { - scheduler.start(); - } - } - + /** * stop the schedular ad all associated tasks */ @@ -244,7 +272,14 @@ public void stop() { scheduler.stop(); } } + + @Override + public void stopService() { + super.stopService(); + stop(); + } + public static void main(String[] args) { LoggingFactory.init(Level.INFO); @@ -262,9 +297,9 @@ public static void main(String[] args) { * cron.addScheduledEvent("59 * * * *","arduino","digitalWrite", 11, 0); */ // every odd minute - String id = cron.addNamedTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1); + String id = cron.addTask("led on", "1-59/2 * * * *", "mega", "digitalWrite", 13, 1); // every event minute - String id2 = cron.addNamedTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0); + String id2 = cron.addTask("led off", "*/2 * * * *", "mega", "digitalWrite", 13, 0); // Runtime.createAndStart("webgui", "WebGui"); @@ -272,4 +307,5 @@ public static void main(String[] args) { Logging.logError(e); } } + } diff --git a/src/main/java/org/myrobotlab/service/config/CronConfig.java b/src/main/java/org/myrobotlab/service/config/CronConfig.java index 5bb0c50ff5..974526c1f2 100644 --- a/src/main/java/org/myrobotlab/service/config/CronConfig.java +++ b/src/main/java/org/myrobotlab/service/config/CronConfig.java @@ -1,11 +1,12 @@ package org.myrobotlab.service.config; -import java.util.LinkedHashMap; +import java.util.ArrayList; +import java.util.List; import org.myrobotlab.service.Cron.Task; public class CronConfig extends ServiceConfig { - public LinkedHashMap tasks = new LinkedHashMap<>(); + public List tasks = new ArrayList<>(); } diff --git a/src/main/resources/resource/WebGui/app/service/js/CronGui.js b/src/main/resources/resource/WebGui/app/service/js/CronGui.js index 80ce7a109b..7fcf13e0ff 100644 --- a/src/main/resources/resource/WebGui/app/service/js/CronGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/CronGui.js @@ -12,7 +12,7 @@ angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope' cronPattern: null, name: null, method: null, - data: null + data: null } // GOOD TEMPLATE TO FOLLOW @@ -34,20 +34,24 @@ angular.module('mrlapp.service.CronGui', []).controller('CronGuiCtrl', ['$scope' } $scope.addNamedTask = function() { - if ($scope.parameters && $scope.parameters.length > 0){ + if ($scope.parameters && $scope.parameters.length > 0) { $scope.newTask.data = JSON.parse($scope.parameters) } else { $scope.newTask.data = null } - - msg.send('addNamedTask', $scope.newTask) + + msg.send('addTask', $scope.newTask) } - + $scope.removeTask = function(id) { msg.send('removeTask', id) } - msg.subscribe(this) } -]) +]).filter('epochToLocalDate', function() { + return function(epochTime) { + return new Date(epochTime).toLocaleString(); + } + ; +}); diff --git a/src/main/resources/resource/WebGui/app/service/views/CronGui.html b/src/main/resources/resource/WebGui/app/service/views/CronGui.html index 78f17783cb..c1f31bd3cd 100644 --- a/src/main/resources/resource/WebGui/app/service/views/CronGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/CronGui.html @@ -8,13 +8,13 @@

Cron Tab

- + - + @@ -52,6 +52,19 @@

Cron Tab

+ + + + + + + + + + + + +
nameid cron service method parameters
History
{{history.id}}{{ history.processedTime | epochToLocalDate }}
From a8dc9636c9d6bbd8a55fe856b0e31e0f8329bee6 Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 15:48:35 -0700 Subject: [PATCH 016/232] forgot one --- .../java/org/myrobotlab/service/Hd44780.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/Hd44780.java b/src/main/java/org/myrobotlab/service/Hd44780.java index da1750d5c2..d20adf6986 100644 --- a/src/main/java/org/myrobotlab/service/Hd44780.java +++ b/src/main/java/org/myrobotlab/service/Hd44780.java @@ -13,6 +13,7 @@ import org.myrobotlab.service.config.Hd44780Config; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; +import org.myrobotlab.service.interfaces.TextListener; import org.slf4j.Logger; /** @@ -24,7 +25,7 @@ * @author Moz4r modified by Ray Edgley. * */ -public class Hd44780 extends Service { +public class Hd44780 extends Service implements TextListener { public final static Logger log = LoggerFactory.getLogger(Hd44780.class); @@ -205,6 +206,16 @@ public void display(String string, int line) { } lcdWriteDataString(string); } + + /** + * display the text + * FIXME - should by default scroll if text is larger than the width of the hd + * @param text + */ + public void display(String text) { + // FIXME - lame, but going to default this way + display(text, 0); + } /** * Clear lcd and set to home. @@ -710,4 +721,11 @@ public ServiceConfig apply(ServiceConfig c) { } return c; } + + + @Override + public void onText(String text) throws Exception { + display(text); + } + } \ No newline at end of file From 879164715a5490cfe5d505809331c1422905f57b Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 18:40:08 -0700 Subject: [PATCH 017/232] Teamwork fix of Hd44780 --- .../java/org/myrobotlab/service/Hd44780.java | 78 ++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/Hd44780.java b/src/main/java/org/myrobotlab/service/Hd44780.java index d20adf6986..0cf65f4eb1 100644 --- a/src/main/java/org/myrobotlab/service/Hd44780.java +++ b/src/main/java/org/myrobotlab/service/Hd44780.java @@ -183,10 +183,12 @@ public void attachPcf8574(Pcf8574 pcf8574) { * */ public void display(String string, int line) { + log.info("display({},{})", string, line); if (!initialized) { init(); } screenContent.put(line, string); + // FIXME a bit sloppy .. should publishText broadcastState(); switch (line) { case 0: @@ -199,17 +201,32 @@ public void display(String string, int line) { setDdramAddress((byte) 0x14); break; case 3: - setDdramAddress((byte) 0x3C); + setDdramAddress((byte) 0x54); break; default: error("line %d is invalid, valid line values are 0 - 3"); } lcdWriteDataString(string); } - + /** - * display the text - * FIXME - should by default scroll if text is larger than the width of the hd + * Write text to the address at preferred location. Remember the line wrap is + * strange for this device. + * + * @param address + * - ddram address position + * @param text + * - the text to write there + */ + public void displayAt(int address, String text) { + setDdramAddress(address); + lcdWriteDataString(text); + } + + /** + * display the text FIXME - should by default scroll if text is larger than + * the width of the hd + * * @param text */ public void display(String text) { @@ -270,7 +287,8 @@ public void init() { } else { log.info("Init I2C Display"); - setInterface(); // this commands ensures we are in 4 bit mode and our commands are in sync. + setInterface(); // this commands ensures we are in 4 bit mode and our + // commands are in sync. setFunction(true, false); // Set the function Control. clearDisplay(); // Clear the Display and set DDRAM address 0. returnHome(); // Set DDRAM address 0 and return display home. @@ -479,13 +497,14 @@ public void clearDisplay() { * @param address */ public void setDdramAddress(int address) { - if (address < 80) { // Make sure the address is in a valid range - lcdWriteCmd((byte) (address | 0b10000000)); - } else { - error("%d Outside allowed DDRAM Address range 0 - 79", address); + if (address < 0 || (address > 40 && address < 63) || address > 108) { + error("%d Outside allowed DDRAM Address range 0 - 108", address); + return; } + lcdWriteCmd((byte) (address | 0b10000000)); } - + + /** * Set the address to read or write data to the Caracter Generator RAM. The * HD44780 has a built in 205 charater generator rom as well as a 8 charater @@ -564,29 +583,37 @@ public boolean getVerifyBusyFlag() { } /** - * This method will first make sure the HD44780 is in a known state - * and that we are syncrnised to the state. - * It does this by setting the module into 8 bit mode - * then setting it back to 4 bit mode. + * This method will first make sure the HD44780 is in a known state and that + * we are syncrnised to the state. It does this by setting the module into 8 + * bit mode then setting it back to 4 bit mode. */ private void setInterface() { - byte Blight = 0b00001000; // The backlight bit is not used by the HD44780 chip. + byte Blight = 0b00001000; // The backlight bit is not used by the HD44780 + // chip. if (!backLight) { Blight = 0; } pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Set to 8 bit mode - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command sleep(10); - pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 bit command - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command + pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 + // bit command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command sleep(10); - pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 bit command - pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command . We should now be in 8 bit mode and in sync + pcf.writeRegister((byte) (0b00110000 | En | Blight)); // Repeat the set to 8 + // bit command + pcf.writeRegister((byte) (0b00110000 | Blight)); // Strobe in command . We + // should now be in 8 bit + // mode and in sync sleep(10); - pcf.writeRegister((byte) (0b00100000 | En | Blight)); // Now set for 4 bit mode. - pcf.writeRegister((byte) (0b00100000 | Blight)); // Strobe in command. In theory, we should now be in 4 bit mode. + pcf.writeRegister((byte) (0b00100000 | En | Blight)); // Now set for 4 bit + // mode. + pcf.writeRegister((byte) (0b00100000 | Blight)); // Strobe in command. In + // theory, we should now be + // in 4 bit mode. sleep(10); } + /** * This method will write the cmd value to the instruction register then wait * until it is ready for the next instruction. Most commands are pretty quick @@ -693,7 +720,7 @@ public static void main(String[] args) { */ @Override public ServiceConfig getConfig() { - Hd44780Config config = (Hd44780Config)super.getConfig(); + Hd44780Config config = (Hd44780Config) super.getConfig(); if (pcfName != null) { config.controller = pcfName; } @@ -721,11 +748,10 @@ public ServiceConfig apply(ServiceConfig c) { } return c; } - - + @Override public void onText(String text) throws Exception { display(text); } - + } \ No newline at end of file From ccceb78781855d24726797f6c931375440c21694 Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 22 Jul 2023 19:28:10 -0700 Subject: [PATCH 018/232] intermediate --- src/main/java/org/myrobotlab/service/WebXR.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/WebXR.java b/src/main/java/org/myrobotlab/service/WebXR.java index 22328d1b39..d194df1e4e 100644 --- a/src/main/java/org/myrobotlab/service/WebXR.java +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -56,10 +56,12 @@ public Pose publishPose(Pose pose) { invoke("publishJointAngles", map); + // TODO - publishQuaternion + // invoke("publishQuaternion", map); + return pose; } - // TODO publishQuaternion public Map publishJointAngles(Map map){ return map; From 0cf9a5209e9250feff3921433352a6926f3dccea Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 2 Sep 2023 21:18:17 -0700 Subject: [PATCH 019/232] initial draft --- .../java/org/myrobotlab/framework/Plan.java | 16 - .../org/myrobotlab/framework/Service.java | 83 +- .../myrobotlab/opencv/OpenCVFilterYolo.java | 12 +- .../java/org/myrobotlab/service/Arduino.java | 2 +- .../org/myrobotlab/service/AudioFile.java | 2 +- .../service/FiniteStateMachine.java | 15 +- .../java/org/myrobotlab/service/InMoov2.java | 1123 ++++++++++------- .../org/myrobotlab/service/InMoov2Arm.java | 2 +- .../org/myrobotlab/service/InMoov2Hand.java | 4 +- .../org/myrobotlab/service/InMoov2Head.java | 4 +- .../org/myrobotlab/service/InMoov2Torso.java | 4 +- .../java/org/myrobotlab/service/NeoPixel.java | 224 ++-- .../java/org/myrobotlab/service/Runtime.java | 90 +- .../service/config/InMoov2Config.java | 207 ++- .../service/config/NeoPixelConfig.java | 2 +- .../service/config/ServiceConfig.java | 7 +- .../service/config/SpeechSynthesisConfig.java | 1 + .../service/data/LedDisplayData.java | 75 +- .../service/interfaces/AudioControl.java | 9 +- .../WebGui/app/service/js/NeoPixelGui.js | 2 +- .../WebGui/app/service/js/RuntimeGui.js | 4 +- .../WebGui/app/service/views/NeoPixelGui.html | 2 +- 22 files changed, 1057 insertions(+), 833 deletions(-) diff --git a/src/main/java/org/myrobotlab/framework/Plan.java b/src/main/java/org/myrobotlab/framework/Plan.java index 92f48b066e..ae26195553 100644 --- a/src/main/java/org/myrobotlab/framework/Plan.java +++ b/src/main/java/org/myrobotlab/framework/Plan.java @@ -142,21 +142,5 @@ public void addRegistry(String service) { runtime.add(service); } - /** - * good to prune trees of peers from starting - expecially if the peers - * require re-configuring - * - * @param startsWith - * - removes RuntimeConfig.registry all services that start with - * input - */ - public void removeStartsWith(String startsWith) { - RuntimeConfig runtime = (RuntimeConfig) config.get("runtime"); - if (runtime == null) { - log.error("removeRegistry - runtime null !"); - return; - } - runtime.removeStartsWith(startsWith); - } } diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 08534f2081..1a3d9e6956 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -893,27 +893,9 @@ public void broadcastState() { } @Override + @Deprecated /* use publishStatus */ public void broadcastStatus(Status status) { - long now = System.currentTimeMillis(); - /* - * if (status.equals(lastStatus) && now - lastStatusTs < - * statusBroadcastLimitMs) { return; } - */ - if (status.name == null) { - status.name = getName(); - } - if (status.level.equals(StatusLevel.ERROR)) { - lastError = status; - lastErrorTs = now; - log.error(status.toString()); - invoke("publishError", status); - } else { - log.info(status.toString()); - } - invoke("publishStatus", status); - lastStatusTs = now; - lastStatus = status; } @Override @@ -1543,7 +1525,7 @@ public QueueStats publishQueueStats(QueueStats stats) { * @return the service */ @Override - public Service publishState() { + public Service publishState() { return this; } @@ -2087,46 +2069,47 @@ public void unsubscribe(String topicName, String topicMethod, String callbackNam @Override public Status error(Exception e) { log.error("status:", e); - Status ret = Status.error(e); - ret.name = getName(); - log.error(ret.toString()); - invoke("publishStatus", ret); - return ret; + Status status = Status.error(e); + status.name = getName(); + log.error(status.toString()); + invoke("publishStatus", status); + return status; } - + @Override public Status error(String format, Object... args) { - Status ret; - ret = Status.error( - String.format( - Objects.requireNonNullElse(format, ""), - args - ) - ); - ret.name = getName(); - log.error(ret.toString()); - lastError = ret; - invoke("publishStatus", ret); - return ret; + String msg = String.format( + Objects.requireNonNullElse(format, ""), + args); + return error(msg); } public Status error(String msg) { - return error(msg, (Object[]) null); + Status status = Status.error(msg); + status.name = getName(); + log.error(status.toString()); + lastError = status; + invoke("publishStatus", status); + return status; } public Status warn(String msg) { - return warn(msg, (Object[]) null); - } - - @Override - public Status warn(String format, Object... args) { - Status status = Status.warn(format, args); + Status status = Status.warn(msg); status.name = getName(); log.warn(status.toString()); invoke("publishStatus", status); return status; } + @Override + public Status warn(String format, Object... args) { + String msg = String.format( + Objects.requireNonNullElse(format, ""), + args); + + return warn(msg); + } + /** * set status broadcasts an info string to any subscribers * @@ -2161,9 +2144,19 @@ public Status info(String format, Object... args) { public Status publishError(Status status) { return status; } + + public Status publishWarn(Status status) { + return status; + } @Override public Status publishStatus(Status status) { + // demux over different channels + if (status.isError()) { + invoke("publishError", status); + } else if (status.isWarn()) { + invoke("publishWarn", status); + } return status; } diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java index ce83566839..a03c9b20ce 100755 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java @@ -33,7 +33,8 @@ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { private static final long serialVersionUID = 1L; - public final static Logger log = LoggerFactory.getLogger(OpenCVFilterYolo.class); + + public transient final static Logger log = LoggerFactory.getLogger(OpenCVFilterYolo.class); protected Boolean running; @@ -43,7 +44,7 @@ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { transient private final OpenCVFrameConverter.ToIplImage grabberConverter = new OpenCVFrameConverter.ToIplImage(); - private float confidenceThreshold = 0.25F; + protected float confidenceThreshold = 0.25F; // the column in the detection matrix that contains the confidence level. (I // think?) // int probability_index = 5; @@ -64,10 +65,11 @@ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { transient private OpenCVFrameConverter.ToIplImage converterToIpl = new OpenCVFrameConverter.ToIplImage(); - boolean debug = false; + protected boolean debug = false; transient private Net net; ArrayList classNames; - public ArrayList lastResult = null; + // quick fix for exploding serialization of Classification + transient public ArrayList lastResult = null; transient private volatile IplImage lastImage = null; private volatile boolean pending = false; transient private Thread classifier; @@ -356,7 +358,7 @@ public void release() { // } } - volatile Object lock = new Object(); + transient volatile Object lock = new Object(); @Override public void enable() { diff --git a/src/main/java/org/myrobotlab/service/Arduino.java b/src/main/java/org/myrobotlab/service/Arduino.java index 89e407c18d..bff3e027ac 100644 --- a/src/main/java/org/myrobotlab/service/Arduino.java +++ b/src/main/java/org/myrobotlab/service/Arduino.java @@ -929,7 +929,7 @@ public List getBoardTypes() { List boardTypes = new ArrayList(); try { - String b = FileIO.resourceToString("Arduino" + File.separator + "boards.txt"); + String b = FileIO.toString(FileIO.gluePaths(getResourceDir(), "boards.txt")); Properties boardProps = new Properties(); boardProps.load(new ByteArrayInputStream(b.getBytes())); diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java index 8b5a3cb828..6a4dc76221 100644 --- a/src/main/java/org/myrobotlab/service/AudioFile.java +++ b/src/main/java/org/myrobotlab/service/AudioFile.java @@ -557,7 +557,7 @@ public AudioFileConfig apply(AudioFileConfig config) { } public double publishPeak(double peak) { - log.info("publishPeak {}", peak); + log.debug("publishPeak {}", peak); return peak; } diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index a093f61565..f4d56d168e 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) @@ -92,6 +97,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) { @@ -168,6 +180,7 @@ public void fire(String event) { if (last != null && !last.equals(current)) { invoke("publishNewState", current.getName()); + history.add(current.getName()); } } catch (Exception e) { log.error("fire threw", e); diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 51dac5ae44..2476bbbeef 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -3,16 +3,18 @@ import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import org.apache.commons.io.FilenameUtils; import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.Plan; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; @@ -28,7 +30,6 @@ import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -52,6 +53,18 @@ public class InMoov2 extends Service implements ServiceLifeCycleL private static final long serialVersionUID = 1L; + protected static final Set stateDefaults = new TreeSet<>(); + + /** + * Setting the beginning time where events are initially set. If the time + * interval has been reached: {@code + * System.currentTimeMillis() > lastFireRandomEvent - stateRandomEventTime + * } then a random event will be fired. If the fsm is in a state that can move + * on that event .. it will go to "random" state + * + */ + protected long stateLastRandomTime = System.currentTimeMillis(); + static String speechRecognizer = "WebkitSpeechRecognition"; /** @@ -93,20 +106,50 @@ public static boolean loadFile(String file) { return true; } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.ERROR); + // identical to command line start + // Runtime.startConfig("inmoov2"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + } catch (Exception e) { + log.error("main threw", e); + } + } + protected transient ProgramAB chatBot; protected List configList; - /** * Configuration from runtime has started. This is when runtime starts * processing a configuration set for the first time since inmoov was started + * + * When configuration is being processed this is true, otherwise false. It's a + * state of the InMoov2 lifecycle, but the FSM isn't guaranteed to be started + * (or configured) at this time, so a member variable was created to guarantee + * this information is available */ protected boolean configStarted = false; - String currentConfigurationName = "default"; + /** + * the config that was processed before booting, if there was one. + */ + String bootedConfig = null; protected transient SpeechRecognizer ear; + protected Map ledDisplayMap = new TreeMap<>(); + + protected List errors = new ArrayList<>(); + + /** + * The finite state machine is core to managing state of InMoov2. There is + * very little benefit gained in having the interactions pub/sub. Therefore, + * there will be a direct reference to the fsm. If different state graph is + * needed, then the fsm can provide that service. + */ + private transient FiniteStateMachine fsm = null; // waiting controable threaded gestures we warn user protected boolean gestureAlreadyStarted = false; @@ -121,14 +164,12 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected LedDisplayData led = new LedDisplayData(); - /** * supported locales */ protected Map locales = null; - protected int maxInactivityTimeSeconds = 120; + protected long stateLastIdleTime = System.currentTimeMillis(); protected transient SpeechSynthesis mouth; @@ -140,9 +181,52 @@ public static boolean loadFile(String file) { protected String voiceSelected; + /** + * prevents being booted more than once + */ + private boolean booted = false; + + protected List peersStarted = new ArrayList<>(); public InMoov2(String n, String id) { super(n, id); + + // add the default InMoov2 state handlers - so the FSM can invoke them + // this is hardcode, because it requires Java methods in InMoov2 + // so it makes sense to hardcode them... + // if a user needs something different, it will happen in pyton-land + // consequence it this will need maintenance if there are new InMoov2 java + // state handlers + stateDefaults.add("wake"); + stateDefaults.add("idle"); + stateDefaults.add("firstInit"); + stateDefaults.add("idle"); + stateDefaults.add("random"); + stateDefaults.add("sleep"); // listens & dreams, no opencv, waits for + // wakeWord, pir active + stateDefaults.add("powerDown"); // stops heartbeat, listening ? + stateDefaults.add("shutdown");// ends mrl + + ledDisplayMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); + ledDisplayMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); + ledDisplayMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); + ledDisplayMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); + ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); + ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); + ledDisplayMap.put("pirOn", new LedDisplayData(80, 200, 90, 3, 100, 30)); + ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); + } + + /** + * pir active ear listening for wakeword + */ + public void idle() { + log.info("idle"); + } + + public void shutdown() { + log.info("shutdown"); + Runtime.shutdown(); } public void addTextListener(TextListener service) { @@ -163,7 +247,9 @@ public InMoov2Config apply(InMoov2Config c) { setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString())); } - loadInitScripts(); + if (c.loadInitScripts) { + loadInitScripts(); + } if (c.loadGestures) { loadGestures(); @@ -181,8 +267,6 @@ public InMoov2Config apply(InMoov2Config c) { return c; } - - @Override public void attachTextListener(String name) { addListener("publishText", name); @@ -203,18 +287,6 @@ public void attachTextPublisher(TextPublisher service) { subscribe(service.getName(), "publishText"); } - public void beginCheckingOnInactivity() { - beginCheckingOnInactivity(maxInactivityTimeSeconds); - } - - public void beginCheckingOnInactivity(int maxInactivityTimeSeconds) { - this.maxInactivityTimeSeconds = maxInactivityTimeSeconds; - // speakBlocking("power down after %s seconds inactivity is on", - // this.maxInactivityTimeSeconds); - log.info("power down after %s seconds inactivity is on", this.maxInactivityTimeSeconds); - addTask("checkInactivity", 5 * 1000, 0, "checkInactivity"); - } - public void cameraOff() { if (opencv != null) { opencv.stopCapture(); @@ -295,23 +367,6 @@ public String captureGesture(String gesture) { return script.toString(); } - public long checkInactivity() { - // speakBlocking("checking"); - long lastActivityTime = getLastActivityTime(); - long now = System.currentTimeMillis(); - long inactivitySeconds = (now - lastActivityTime) / 1000; - if (inactivitySeconds > maxInactivityTimeSeconds) { - // speakBlocking("%d seconds have passed without activity", - // inactivitySeconds); - powerDown(); - } else { - // speakBlocking("%d seconds have passed without activity", - // inactivitySeconds); - info("checking checkInactivity - %d seconds have passed without activity", inactivitySeconds); - } - return lastActivityTime; - } - public void closeAllImages() { // FIXME - follow this pattern ? // CON npe possible although unlikely @@ -405,7 +460,7 @@ public String execGesture(String gesture) { subscribe("python", "publishStatus", this.getName(), "onGestureStatus"); startedGesture(gesture); lastGestureExecuted = gesture; - Python python = (Python)Runtime.getService("python"); + Python python = (Python) Runtime.getService("python"); if (python == null) { error("python service not started"); return null; @@ -451,9 +506,30 @@ public void finishedGesture(String nameOfGesture) { } } - // FIXME - this isn't the callback for fsm - why is it needed here ? - public void fire(String event) { - invoke("publishEvent", event); + /** + * used to configure a flashing event - could use configuration to signal + * different colors and states + * + * @return + */ + public void flash() { + // FIXME - this should be checking a protected "state" + if (!configStarted) { + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + } + } + + public void flash(int r, int g, int b, int count) { + // FIXME - this should be checking a protected "state" + if (!configStarted) { + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); + } + } } public void fullSpeed() { @@ -500,30 +576,32 @@ public InMoov2Head getHead() { * @return the timestamp of the last activity time. */ public Long getLastActivityTime() { - try { - - Long lastActivityTime = 0L; - - Long head = (Long) sendToPeerBlocking("head", "getLastActivityTime", getName()); - Long leftArm = (Long) sendToPeerBlocking("leftArm", "getLastActivityTime", getName()); - Long rightArm = (Long) sendToPeerBlocking("rightArm", "getLastActivityTime", getName()); - Long leftHand = (Long) sendToPeerBlocking("leftHand", "getLastActivityTime", getName()); - Long rightHand = (Long) sendToPeerBlocking("rightHand", "getLastActivityTime", getName()); - Long torso = (Long) sendToPeerBlocking("torso", "getLastActivityTime", getName()); - - lastActivityTime = Math.max(head, leftArm); - lastActivityTime = Math.max(lastActivityTime, rightArm); - lastActivityTime = Math.max(lastActivityTime, leftHand); - lastActivityTime = Math.max(lastActivityTime, rightHand); - lastActivityTime = Math.max(lastActivityTime, torso); - - return lastActivityTime; - - } catch (Exception e) { - error(e); - return null; + Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null; + Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null; + Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null; + Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null; + Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null; + Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null; + + Long lastActivityTime = null; + + if (head != null || leftArm != null || rightArm != null || leftHand != null || rightHand != null || torso != null) { + lastActivityTime = 0L; + if (head != null) + lastActivityTime = Math.max(lastActivityTime, head); + if (leftArm != null) + lastActivityTime = Math.max(lastActivityTime, leftArm); + if (rightArm != null) + lastActivityTime = Math.max(lastActivityTime, rightArm); + if (leftHand != null) + lastActivityTime = Math.max(lastActivityTime, leftHand); + if (rightHand != null) + lastActivityTime = Math.max(lastActivityTime, rightHand); + if (torso != null) + lastActivityTime = Math.max(lastActivityTime, torso); } + return lastActivityTime; } public InMoov2Arm getLeftArm() { @@ -602,10 +680,6 @@ public InMoov2Torso getTorso() { return (InMoov2Torso) getPeer("torso"); } - public InMoov2Config getTypedConfig() { - return (InMoov2Config) config; - } - public void halfSpeed() { sendToPeer("head", "setSpeed", 25.0, 25.0, 25.0, 25.0, 100.0, 25.0); sendToPeer("rightHand", "setSpeed", 30.0, 30.0, 30.0, 30.0, 30.0, 30.0); @@ -615,15 +689,6 @@ public void halfSpeed() { sendToPeer("torso", "setSpeed", 20.0, 20.0, 20.0); } - /** - * execute python scripts in the init directory on startup of the service - * - * @throws IOException - */ - public void loadInitScripts() throws IOException { - loadScripts(getResourceDir() + fs + "init"); - } - public boolean isCameraOn() { if (opencv != null) { if (opencv.isCapturing()) { @@ -687,6 +752,15 @@ public boolean loadGestures(String directory) { return true; } + /** + * execute python scripts in the init directory on startup of the service + * + * @throws IOException + */ + public void loadInitScripts() throws IOException { + loadScripts(getResourceDir() + fs + "init"); + } + /** * Generalized directory python script loading method * @@ -722,7 +796,17 @@ public boolean accept(File dir, String name) { } public void moveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - invoke("publishMoveArm", which, bicep, rotate, shoulder, omoplate); + HashMap map = new HashMap<>(); + Optional.ofNullable(bicep).ifPresent(value -> map.put("bicep", value)); + Optional.ofNullable(rotate).ifPresent(value -> map.put("rotate", value)); + Optional.ofNullable(shoulder).ifPresent(value -> map.put("shoulder", value)); + Optional.ofNullable(omoplate).ifPresent(value -> map.put("omoplate", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftArm", map); + } else { + invoke("publishMoveRightArm", map); + } } public void moveEyelids(Double eyelidleftPos, Double eyelidrightPos) { @@ -738,7 +822,19 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D } public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist); + HashMap map = new HashMap<>(); + Optional.ofNullable(thumb).ifPresent(value -> map.put("thumb", value)); + Optional.ofNullable(index).ifPresent(value -> map.put("index", value)); + Optional.ofNullable(majeure).ifPresent(value -> map.put("majeure", value)); + Optional.ofNullable(ringFinger).ifPresent(value -> map.put("ringFinger", value)); + Optional.ofNullable(pinky).ifPresent(value -> map.put("pinky", value)); + Optional.ofNullable(wrist).ifPresent(value -> map.put("wrist", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftHand", map); + } else { + invoke("publishMoveRightHand", map); + } } public void moveHead(Double neck, Double rothead) { @@ -754,26 +850,33 @@ public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Doub } public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - invoke("publishMoveHead", neck, rothead, eyeX, eyeY, jaw, rollNeck); + HashMap map = new HashMap<>(); + Optional.ofNullable(neck).ifPresent(value -> map.put("neck", value)); + Optional.ofNullable(rothead).ifPresent(value -> map.put("rothead", value)); + Optional.ofNullable(eyeX).ifPresent(value -> map.put("eyeX", value)); + Optional.ofNullable(eyeY).ifPresent(value -> map.put("eyeY", value)); + Optional.ofNullable(jaw).ifPresent(value -> map.put("jaw", value)); + Optional.ofNullable(rollNeck).ifPresent(value -> map.put("rollNeck", value)); + invoke("publishMoveHead", map); } public void moveHead(Integer neck, Integer rothead, Integer rollNeck) { moveHead((double) neck, (double) rothead, null, null, null, (double) rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead) { + public void moveHeadBlocking(Double neck, Double rothead) { moveHeadBlocking(neck, rothead, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { + public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { moveHeadBlocking(neck, rothead, null, null, null, rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { moveHeadBlocking(neck, rothead, eyeX, eyeY, jaw, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { try { sendBlocking(getPeerName("head"), "moveToBlocking", neck, rothead, eyeX, eyeY, jaw, rollNeck); } catch (Exception e) { @@ -806,8 +909,11 @@ public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer } public void moveTorso(Double topStom, Double midStom, Double lowStom) { - // the "right" way - invoke("publishMoveTorso", topStom, midStom, lowStom); + HashMap map = new HashMap<>(); + Optional.ofNullable(topStom).ifPresent(value -> map.put("topStom", value)); + Optional.ofNullable(midStom).ifPresent(value -> map.put("midStom", value)); + Optional.ofNullable(lowStom).ifPresent(value -> map.put("lowStom", value)); + invoke("publishMoveTorso", map); } public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { @@ -827,6 +933,11 @@ public PredicateEvent onChangePredicate(PredicateEvent event) { return event; } + public void onConfigFinished(String configName) { + log.info("onConfigFinished"); + invoke("publishBoot"); + } + /** * comes in from runtime which owns the config list * @@ -843,10 +954,10 @@ public void onCreated(String fullname) { log.info("{} created", fullname); } - public void onFinishedConfig(String configName) { - log.info("onFinishedConfig"); - // invoke("publishEvent", "configFinished"); - invoke("publishFinishedConfig", configName); + public void onError(Status status) { + if (errors.size() < 100) { + errors.add(status); + } } public void onGestureStatus(Status status) { @@ -854,10 +965,66 @@ public void onGestureStatus(Status status) { error("I cannot execute %s, please check logs", lastGestureExecuted); } finishedGesture(lastGestureExecuted); - + unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + public void onHeartbeat() { + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + + if (config.heartbeatFlash) { + if (ledDisplayMap.get("heartbeat") != null) { + LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); + invoke("publishFlash", heartbeat); + } + } + + if (config.batteryLevelCheck) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + } + } + + // flash error until errors are cleared + if (config.healthCheckFlash && errors.size() > 0) { + if (ledDisplayMap.containsKey("error")) { + invoke("publishFlash", ledDisplayMap.get("error")); + } + } + + // interval event firing ... + // sequence of these makes a difference and is hardcode fyi + + // if there is activity in the idle time window - reset idle time to current + // time + // so idle event will not fire until at least inverval after activity + Long lastActivityTime = getLastActivityTime(); + + if (lastActivityTime != null && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } + + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fsm.fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } + + // interval event firing + if (System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + fsm.fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } + + } + @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -878,16 +1045,93 @@ public void onJoystickInput(JoystickData input) throws Exception { invoke("publishEvent", "joystick"); } + public void onMoveHead(Map map) { + InMoov2Head head = (InMoov2Head) getPeer("head"); + if (head != null) { + head.onMove(map); + } + } + + public void onMoveLeftArm(Map map) { + InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); + if (leftArm != null) { + leftArm.onMove(map); + } + } + + public void onMoveLeftHand(Map map) { + InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); + if (leftHand != null) { + leftHand.onMove(map); + } + } + + public void onMoveRightArm(Map map) { + InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); + if (rightArm != null) { + rightArm.onMove(map); + } + } + + public void onMoveRightHand(Map map) { + InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); + if (rightHand != null) { + rightHand.onMove(map); + } + } + + public void onMoveTorso(Map map) { + InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); + if (torso != null) { + torso.onMove(map); + } + } + + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param state + * @return + */ public String onNewState(String state) { log.error("onNewState {}", state); + invoke("publishEvent", String.format("ON STATE %s", state)); + + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(state)) { + invoke(state); + } + + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library + // put configurable filter here ! // state substitutions ? // let python subscribe directly to fsm.publishNewState + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library + // if - invoke(state); + // invoke(state); // depending on configuration .... // call python ? // fire fsm events ? @@ -900,36 +1144,62 @@ public OpenCVData onOpenCVData(OpenCVData data) { return data; } + /** + * onPeak volume callback TODO - maybe make it variable with volume ? + * @param volume + */ + public void onPeak(double volume) { + if (config.neoPixelFlashWhenSpeaking && !configStarted) { + if (volume > 0.5) { + if (ledDisplayMap.get("onPeakColor") != null) { + LedDisplayData onPeakColor = ledDisplayMap.get("onPeakColor"); + invoke("publishFlash", onPeakColor); + } + } + } + } + /** * initial callback for Pir sensor Default behavior will be: send fsm event * onPirOn flash neopixel */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.interval = 500; - // FIXME flash on config.flashOnBoot - invoke("publishFlash"); + // FIXME - this should be checking a protected "state" + if (!configStarted) { + if (ledDisplayMap.get("pirOn") != null) { + LedDisplayData pirOn = ledDisplayMap.get("pirOn"); + invoke("publishFlash", pirOn); + } + } // pirOn event vs wake event - invoke("publishEvent", "WAKE"); + + // FIXME - this is state related + String topic = chatBot.getPredicate("topic"); + // if sleeping then publishe a wake event + if ("sleep".equals(topic)) { + invoke("publishEvent", "wake"); + } } - // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python - // yet provides a stable default, which can be fully replaced - // Works using common pub/sub rules - // TODO - this is a loopback power up - // its replaceable by typical subscription rules - public void onPowerUp() { - // CON - type aware - NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); - // CON - necessary NPE checking - if (neoPixel != null) { - neoPixel.setColor(0, 130, 0); - neoPixel.playAnimation("Larson Scanner"); + public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) + + rest(); + purgeTasks(); // including heartbeat + disable(); + + if (chatBot != null) { + chatBot.sleep(); } + + if (ear != null) { + // FIXME - bad remove it - what is needed ? + // i think this is legacy wake word + ear.lockOutAllGrammarExcept("power up"); + } + } @Override @@ -955,16 +1225,6 @@ public boolean onSense(boolean b) { return b; } - /** - * runtime re-publish relay - * - * @param configName - */ - public void onStartConfig(String configName) { - log.info("onStartConfig"); - invoke("publishStartConfig", configName); - } - /** * Part of service life cycle - a new servo has been started * @@ -974,121 +1234,16 @@ public void onStartConfig(String configName) { */ @Override public void onStarted(String name) { - InMoov2Config c = (InMoov2Config) config; - - log.info("onStarted {}", name); try { - Runtime runtime = Runtime.getInstance(); log.info("onStarted {}", name); -// BAD IDEA - better to ask for a system report or an error report -// if (runtime.isProcessingConfig()) { -// invoke("publishEvent", "CONFIG STARTED"); -// } - String peerKey = getPeerKey(name); - if (peerKey == null) { - // service not a peer - return; - } - - if (runtime.isProcessingConfig() && !configStarted) { - invoke("publishEvent", "CONFIG STARTED " + runtime.getConfigName()); - configStarted = true; - } - - invoke("publishEvent", "STARTED " + peerKey); - - switch (peerKey) { - case "audioPlayer": - break; - case "chatBot": - ProgramAB chatBot = (ProgramAB) Runtime.getService(name); - chatBot.attachTextListener(getPeerName("htmlFilter")); - startPeer("htmlFilter"); - break; - case "controller3": - break; - case "controller4": - break; - case "ear": - AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name); - ear.attachTextListener(getPeerName("chatBot")); - break; - case "eyeTracking": - break; - case "fsm": - break; - case "gpt3": - break; - case "head": - addListener("publishMoveHead", name); - break; - case "headTracking": - break; - case "htmlFilter": - TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name); - htmlFilter.attachTextListener(getPeerName("mouth")); - break; - case "imageDisplay": - break; - case "leap": - break; - case "left": - break; - case "leftArm": - addListener("publishMoveLeftArm", name, "onMoveArm"); - break; - case "leftHand": - addListener("publishMoveLeftHand", name, "onMoveHand"); - break; - case "mouth": - mouth = (AbstractSpeechSynthesis) Runtime.getService(name); - mouth.attachSpeechListener(getPeerName("ear")); - break; - case "mouthControl": - break; - case "neoPixel": - break; - case "opencv": - subscribeTo(name, "publishOpenCVData"); - break; - case "openni": - break; - case "openWeatherMap": - break; - case "pid": - break; - case "pir": - break; - case "random": - break; - case "right": - break; - case "rightArm": - addListener("publishMoveRightArm", name, "onMoveArm"); - break; - case "rightHand": - addListener("publishMoveRightHand", name, "onMoveHand"); - break; - case "servoMixer": - break; - case "simulator": - break; - case "torso": - addListener("publishMoveTorso", name); - break; - case "ultrasonicRight": - break; - case "ultrasonicLeft": - break; - default: - log.warn("unknown peer %s not hanled in onStarted", peerKey); - break; + if (peerKey != null) { + peersStarted.add(peerKey); } - // type processing for Servo + // new servo ServiceInterface si = Runtime.getService(name); if ("Servo".equals(si.getSimpleName())) { log.info("sending setAutoDisable true to {}", name); @@ -1103,6 +1258,7 @@ public void onStarted(String name) { @Override public void onStopped(String name) { + log.info("service {} has stopped"); // using release peer for peer releasing // FIXME - auto remove subscriptions of peers? } @@ -1117,38 +1273,6 @@ public void onText(String text) { invoke("publishText", text); } - // TODO FIX/CHECK this, migrate from python land - public void powerDown() { - - rest(); - purgeTasks(); - disable(); - - if (ear != null) { - ear.lockOutAllGrammarExcept("power up"); - } - - // FIXME - DO NOT DO THIS !!!! SIMPLY PUBLISH A POWER DOWN EVENT AND PYTHON - // CAN SUBSCRIBE - // AND MAINTAIN A SET OF onPowerDown: callback methods - python.execMethod("power_down"); - } - - // TODO FIX/CHECK this, migrate from python land - // FIXME - defaultPowerUp switchable + override - public void powerUp() { - enable(); - rest(); - - if (ear != null) { - ear.clearLock(); - } - - beginCheckingOnInactivity(); - - python.execMethod("power_up"); - } - /** * easy utility to publishMessage * @@ -1161,16 +1285,11 @@ public void publish(String name, String method, Object... data) { invoke("publishMessage", msg); } - public String publishStartConfig(String configName) { - info("config %s started", configName); - invoke("publishEvent", "CONFIG STARTED " + configName); - return configName; + public double publishBatteryLevel(double d) { + return d; } - public String publishFinishedConfig(String configName) { - info("config %s finished", configName); - invoke("publishEvent", "CONFIG LOADED " + configName); - + public String publishConfigFinished(String configName) { return configName; } @@ -1195,30 +1314,31 @@ public String publishEvent(String event) { return String.format("SYSTEM_EVENT %s", event); } - /** - * used to configure a flashing event - could use configuration to signal - * different colors and states - * - * @return - */ - public LedDisplayData publishFlash() { - return led; + public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + return data; } - public String publishHeartbeat() { - led.action = "flash"; - led.red = 180; - led.green = 10; - led.blue = 30; - led.count = 1; - led.interval = 50; - invoke("publishFlash"); - return getName(); + public LedDisplayData publishFlash(LedDisplayData data) { + return data; + } + + /** + * health check which is a scheduled task at an interval of 1 second + */ + public void publishHeartbeat() { + log.debug("publishHeartbeat"); } /** - * A more extensible interface point than publishEvent - * FIXME - create interface for this + * A more extensible interface point than publishEvent FIXME - create + * interface for this * * @param msg * @return @@ -1227,94 +1347,57 @@ public Message publishMessage(Message msg) { return msg; } - public HashMap publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); - if ("left".equals(which)) { - invoke("publishMoveLeftArm", bicep, rotate, shoulder, omoplate); - } else { - invoke("publishMoveRightArm", bicep, rotate, shoulder, omoplate); - } + public Map publishMoveHead(Map map) { return map; } - public HashMap publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("which", which); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - if ("left".equals(which)) { - invoke("publishMoveLeftHand", thumb, index, majeure, ringFinger, pinky, wrist); - } else { - invoke("publishMoveRightHand", thumb, index, majeure, ringFinger, pinky, wrist); - } + public Map publishMoveLeftArm(Map map) { return map; } - public HashMap publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - HashMap map = new HashMap<>(); - map.put("neck", neck); - map.put("rothead", rothead); - map.put("eyeX", eyeX); - map.put("eyeY", eyeY); - map.put("jaw", jaw); - map.put("rollNeck", rollNeck); + public Map publishMoveLeftHand(Map map) { return map; } - public HashMap publishMoveLeftArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveRightArm(Map map) { return map; } - public HashMap publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); + public Map publishMoveRightHand(Map map) { return map; } - public HashMap publishMoveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveTorso(Map map) { return map; } - public HashMap publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - return map; + public String publishNewState(String state) { + log.info("publishNewState {}", state); + return state; } - public HashMap publishMoveTorso(Double topStom, Double midStom, Double lowStom) { - HashMap map = new HashMap<>(); - map.put("topStom", topStom); - map.put("midStom", midStom); - map.put("lowStom", lowStom); - return map; + public String publishPlayAudioFile(String filename) { + return filename; + } + + /** + * if inactivityTime configured, this event is published after there has not + * been in activity since. + */ + public void publishInactivity() { + log.info("publishInactivity"); + fsm.fire("inactvity"); + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + + } + + public String getState() { + return fsm.getCurrent(); } /** @@ -1502,7 +1585,7 @@ public void setOpenCV(OpenCV opencv) { } public boolean setPirPlaySounds(boolean b) { - getTypedConfig().pirPlaySounds = b; + config.pirPlaySounds = b; return b; } @@ -1541,6 +1624,11 @@ public String setSpeechType(String speechType) { return speechType; } + // ----------------------------------------------------------------------------- + // These are methods added that were in InMoov1 that we no longer had in + // InMoov2. + // From original InMoov1 so we don't loose the + public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "setSpeed", topStom, midStom, lowStom); } @@ -1562,11 +1650,6 @@ public void setVoice(String name) { } } - // ----------------------------------------------------------------------------- - // These are methods added that were in InMoov1 that we no longer had in - // InMoov2. - // From original InMoov1 so we don't loose the - public void sleeping() { log.error("sleeping"); } @@ -1722,7 +1805,8 @@ public void startedGesture(String nameOfGesture) { } public void startHeartbeat() { - addTask(1000, "publishHeartbeat"); + addTask(config.heartbeatInterval, "publishHeartbeat"); + config.heartbeat = true; } // TODO - general objective "might" be to reduce peers down to something @@ -1749,6 +1833,7 @@ public SpeechSynthesis startMouth() { } mouth.attachSpeechRecognizer(ear); + // mouth.attach(htmlFilter); // same as chatBot not needed // this.attach((Attachable) mouth); @@ -1784,36 +1869,76 @@ public ServiceInterface startPeer(String peer) { @Override public void startService() { super.startService(); + fsm = (FiniteStateMachine) startPeer("fsm"); + fsm.init(); - InMoov2Config c = (InMoov2Config) config; Runtime runtime = Runtime.getInstance(); // get service start and release life cycle events runtime.attachServiceLifeCycleListener(getName()); + subscribe("runtime", "publishConfigList"); - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); + // subscribe to config processing events + // runtime callbacks publish the same a local + subscribe("runtime", "publishConfigStarted", "publishConfigStarted"); + subscribe("runtime", "publishConfigFinished", "publishConfigFinished"); + + runtime.invoke("publishConfigList"); + + // iterate through existing started service + // add them to peers booted + for (String name : Runtime.getServiceNames()) { + String peerKey = getPeerKey(name); + if (peerKey != null) { + peersStarted.add(peerKey); } } - // get events of new services and shutdown - subscribe("runtime", "shutdown"); - // power up loopback subscription - addListener(getName(), "powerUp"); - - - subscribe("runtime", "publishConfigList"); if (runtime.isProcessingConfig()) { - invoke("publishEvent", "configStarted"); + // if InMoov2 was started as part of a config set + // set here so boot can be delayed until the config + // set is done + configStarted = true; + bootedConfig = runtime.getConfigName(); + } else { + invoke("publishBoot"); + } + } + + public void publishBoot() { + log.info("publishBoot"); + } + + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + synchronized public void onBoot() { + + // thinking you shouldn't "boot" twice ? + if (booted) { + log.warn("will not boot again"); + return; } - subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); + booted = true; - // chatbot getresponse attached to publishEvent - addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) try { // copy config if it doesn't already exist String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); @@ -1836,7 +1961,150 @@ public void startService() { error(e); } - runtime.invoke("publishConfigList"); + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + neoPixel.clear(); + neoPixel.setColor(0, 130, 0); + neoPixel.playAnimation("Larson Scanner"); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (bootedConfig != null) { + // configuration was processed before booting + invoke("publishEvent", "CONFIG STARTED " + bootedConfig); + } + + // publish config started + + for (String peerKey : peersStarted) { + invoke("publishEvent", "STARTED " + peerKey); + } + + if (bootedConfig != null) { + // configuration was processed before booting + invoke("publishEvent", "CONFIG LOADED " + bootedConfig); + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + + fsm.fire("wake"); + + } + + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + + /** + * ear still listening pir still active + */ + public void sleep() { + log.info("sleep"); + } + + public void wake() { + log.info("wake"); + // do waking things - based on config + + // blink + + // wake gesture + // callback + // imoov2[{name}]["onWake"](this) + /** + *
+     i01.speakBlocking("I was sleeping")
+     lookrightside()
+     sleep(2)
+     lookleftside()
+     sleep(4)
+     relax()
+     ear.clearLock()
+     sleep(2)
+     i01.finishedGesture()
+     * 
+ */ + + /** + *
+     * // legacy
+     * enable();
+     * rest();
+     * 
+     * if (ear != null) {
+     *   ear.clearLock();
+     * }
+     * 
+     * // beginCheckingOnInactivity();
+     * // BAD BAD BAD !!!
+     * publishEvent("powerUp"); // before or after loopback
+     * 
+ **/ + // was a relax gesture .. might want to ask about it .. + + // if ear start listening + AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) getPeer("ear"); + if (ear != null) { + ear.startListening(); + } + + // attempt recognize where its at + + // attempt to recognize people + + // look for activity + + // say hello + + // start animation (configurable) + + rest(); + + // should "session be determined by recognition?" + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + + if (chatBot != null) { + String firstinit = chatBot.getPredicate("firstinit"); + // wtf - "ok" really, for a boolean? + if (!"ok".equals(firstinit)) { + fsm.fire("firstInit"); + } + } + } + + public void firstInit() { + log.info("firstInit"); + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + if (chatBot != null) { + chatBot.getResponse("FIRST_INIT"); + } } public void startServos() { @@ -1870,6 +2138,7 @@ public void stopGesture() { public void stopHeartbeat() { purgeTask("publishHeartbeat"); + config.heartbeat = false; } public void stopNeopixelAnimation() { @@ -1911,90 +2180,4 @@ public void waitTargetPos() { sendToPeer("torso", "waitTargetPos"); } - - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.ERROR); - // Platform.setVirtual(true); - // Runtime.start("s01", "Servo"); - // Runtime.start("intro", "Intro"); - - // Runtime.startConfig("pr-1213-1"); - - Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); - - boolean done = true; - if (done) { - return; - } - - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - // webgui.setSsl(true); - webgui.autoStartBrowser(false); - // webgui.setPort(8888); - webgui.startService(); - - Runtime.start("python", "Python"); - // Runtime.start("ros", "Ros"); - Runtime.start("intro", "Intro"); - // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - // i01.startPeer("simulator"); - // Runtime.startConfig("i01-05"); - // Runtime.startConfig("pir-01"); - - // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); - // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - - - // polly.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - // i01.startPeer("mouth"); - // i01.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - - Runtime.start("python", "Python"); - - // i01.startSimulator(); - Plan plan = Runtime.load("webgui", "WebGui"); - // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui"); - // webgui.autoStartBrowser = false; - Runtime.startConfig("webgui"); - Runtime.start("webgui", "WebGui"); - - Random random = (Random) Runtime.start("random", "Random"); - - random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - - random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - - random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - - random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); - random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); - - random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); - random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); - - random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); - random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); - - random.save(); - -// i01.startChatBot(); -// -// i01.startAll("COM3", "COM4"); - Runtime.start("python", "Python"); - - } catch (Exception e) { - log.error("main threw", e); - } - } - } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java index a73f5527b5..6305eb984a 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java @@ -89,7 +89,7 @@ public static DHRobotArm getDHRobotArm(String name, String side) { return arm; } - public void onMoveArm(HashMap map) { + public void onMove(Map map) { moveTo(map.get("bicep"), map.get("rotate"), map.get("shoulder"), map.get("omoplate")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java index b1744ad03e..a4c1b35bae 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java @@ -2,9 +2,9 @@ import java.io.File; import java.io.IOException; -import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.myrobotlab.framework.Registration; @@ -489,7 +489,7 @@ public LeapData onLeapData(LeapData data) { return data; } - public void onMoveHand(HashMap map) { + public void onMove(Map map) { moveTo(map.get("thumb"), map.get("index"), map.get("majeure"), map.get("majeure"), map.get("pinky"), map.get("wrist")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index be1a72f793..63b34b1198 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -2,7 +2,7 @@ import java.io.File; import java.io.IOException; -import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import org.myrobotlab.framework.Service; @@ -199,7 +199,7 @@ public void lookAt(Double x, Double y, Double z) { log.info("object distance is {},rothead servo {},neck servo {} ", distance, rotation, colatitude); } - public void onMoveHead(HashMap map) { + public void onMove(Map map) { moveTo(map.get("neck"), map.get("rothead"), map.get("eyeX"), map.get("eyeY"), map.get("jaw"), map.get("rollNeck")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java index d2607a1033..d6f0fcc1c4 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java @@ -2,7 +2,7 @@ import java.io.File; import java.io.IOException; -import java.util.HashMap; +import java.util.Map; import org.myrobotlab.framework.Service; import org.myrobotlab.io.FileIO; @@ -93,7 +93,7 @@ public void disable() { lowStom.disable(); } - public void onMoveTorso(HashMap map) { + public void onMove(Map map) { moveTo(map.get("topStom"), map.get("midStom"), map.get("lowStom")); } diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e13daf273d..05a2106a5d 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -28,8 +28,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; @@ -46,11 +47,13 @@ public class NeoPixel extends Service implements NeoPixelControl { + private BlockingQueue displayQueue = new ArrayBlockingQueue<>(200); + /** * Thread to do animations Java side and push the changing of pixels to the * neopixel */ - private class AnimationRunner implements Runnable { + private class Worker implements Runnable { boolean running = false; @@ -60,15 +63,31 @@ private class AnimationRunner implements Runnable { public void run() { try { running = true; - while (running) { - equalizer(); - Double wait_ms_per_frame = fpsToWaitMs(speedFps); - sleep(wait_ms_per_frame.intValue()); + LedDisplayData led = displayQueue.take(); + // save existing state if necessary .. + // stop animations if running + // String lastAnimation = currentAnimation; + if (led.count > 0) { + + clear(); + for (int count = 0; count < led.count; count++) { + fill(led.red, led.green, led.blue); + sleep(led.timeOn); + clear(); + sleep(led.timeOff); + } + } + // start animations + // playAnimation(lastAnimation); } + } catch (InterruptedException ex) { + log.info("shutting down worker"); } catch (Exception e) { error(e); stop(); + } finally { + running = false; } } @@ -81,6 +100,9 @@ public synchronized void start() { public synchronized void stop() { running = false; + if (thread != null) { + thread.interrupt(); + } thread = null; } } @@ -153,7 +175,7 @@ public int[] flatten() { /** * thread for doing off board and in memory animations */ - protected final AnimationRunner animationRunner; + protected final Worker worker; /** * current selected red value @@ -230,7 +252,13 @@ public int[] flatten() { */ protected int brightness = 255; - final Map animations = new HashMap<>(); + protected final Map animations = new HashMap<>(); + + protected long flashTimeOn = 300; + + protected long flashTimeOff = 300; + + protected int flashCount = 1; public Set getAnimations() { return animations.keySet(); @@ -239,7 +267,7 @@ public Set getAnimations() { public NeoPixel(String n, String id) { super(n, id); registerForInterfaceChange(NeoPixelController.class); - animationRunner = new AnimationRunner(); + worker = new Worker(); animations.put("Stop", 1); animations.put("Color Wipe", 2); animations.put("Larson Scanner", 3); @@ -249,8 +277,6 @@ public NeoPixel(String n, String id) { animations.put("Rainbow Cycle", 7); animations.put("Flash Random", 8); animations.put("Ironman", 9); - // > 99 is java side animations - animations.put("Equalizer", 100); } @Override @@ -306,8 +332,6 @@ public void clear() { return; } - // stop java animations - animationRunner.stop(); // stop on board controller animations setAnimation(0, 0, 0, 0, speedFps); @@ -361,101 +385,51 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - public void equalizer() { - equalizer(null, null); + public void onLedDisplay(LedDisplayData data) { + displayQueue.add(data); } - public void equalizer(Long wait_ms_per_frame, Integer range) { - - if (controller == null) { - log.warn("controller not set"); - return; - } - - if (wait_ms_per_frame == null) { - wait_ms_per_frame = 25L; - } - - if (range == null) { - range = 25; - } - - Random rand = new Random(); - int c = rand.nextInt(range); - - fillMatrix(red, green, blue, white); - - if (c < 18) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); - } - - fillMatrix(red, green, blue, white); - - if (c < 16) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); - } - - if (c < 12) { - setMatrix(1, 0, 0, 0); - setMatrix(6, 0, 0, 0); - } - - if (c < 8) { - setMatrix(2, 0, 0, 0); - setMatrix(5, 0, 0, 0); - } - - writeMatrix(); + public void flash() { + flash(red, green, blue, flashCount, flashTimeOn, flashTimeOff); + } + public void flash(int r, int g, int b) { + flash(r, g, b, flashCount , flashTimeOn, flashTimeOff); } - public void onLedDisplay(LedDisplayData data) { - - if ("flash".equals(data.action)) { - flash(data.count, data.interval, data.red, data.green, data.blue); - } - + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, flashTimeOn, flashTimeOff); } - public void flash(int count, long interval, int r, int g, int b) { - long delay = 0; - for (int i = 0; i < count; ++i) { - addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", r, g, b); - delay+= interval/2; - addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); - delay+= interval/2; - } + public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + displayQueue.add(data); } - + public void onFlash(LedDisplayData data) { + displayQueue.add(data); + } + public void flashBrightness(double brightNess) { - NeoPixelConfig c = (NeoPixelConfig)config; + NeoPixelConfig c = (NeoPixelConfig) config; - // FIXME - these need to be moved into config -// int count = 2; -// int interval = 75; - setBrightness((int)brightNess); + setBrightness((int) brightNess); fill(red, green, blue); - -// long delay = 0; -// for (int i = 0; i < count; ++i) { -// addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", red, green, blue); -// delay+= interval/2; -// addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); -// delay+= interval/2; -// } - + if (c.autoClear) { purgeTask("clear"); // and start our countdown addTaskOneShot(c.idleTimeout, "clear"); } - } - + public void fill(int r, int g, int b) { fill(0, pixelCount, r, g, b, null); } @@ -465,8 +439,8 @@ public void fill(int beginAddress, int count, int r, int g, int b) { } public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { - NeoPixelConfig c = (NeoPixelConfig)config; - + NeoPixelConfig c = (NeoPixelConfig) config; + if (w == null) { w = 0; } @@ -477,7 +451,7 @@ public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { return; } np2.neoPixelFill(getName(), beginAddress, count, r, g, b, w); - + if (c.autoClear) { purgeTask("clear"); // and start our countdown @@ -580,20 +554,22 @@ public int getRed() { @Override public void playAnimation(String animation) { + if (animation == null) { + log.info("playAnimation null"); + return; + } + + if (animation.equals(currentAnimation)) { + log.info("already playing {}", currentAnimation); + return; + } if (animations.containsKey(animation)) { currentAnimation = animation; - if (animations.get(animation) < 99) { - setAnimation(animations.get(animation), red, green, blue, speedFps); - } else { - // only 1 java side animation at the moment - equalizer(); - animationRunner.start(); - } + setAnimation(animations.get(animation), red, green, blue, speedFps); } else { error("could not find animation %s", animation); } - broadcastState(); } public void stopAnimation() { @@ -601,6 +577,7 @@ public void stopAnimation() { } @Override + @Deprecated /* use playAnimation */ public void setAnimation(int animation, int red, int green, int blue, int speedFps) { if (speedFps > maxFps) { speedFps = maxFps; @@ -618,7 +595,6 @@ public void setAnimation(int animation, int red, int green, int blue, int speedF nc2.neoPixelSetAnimation(getName(), animation, red, green, blue, 0, wait_ms_per_frame.intValue()); if (animation == 1) { currentAnimation = null; - animationRunner.stop(); } broadcastState(); } @@ -653,7 +629,7 @@ public void setBlue(int blue) { } public void setBrightness(int value) { - NeoPixelConfig c = (NeoPixelConfig)config; + NeoPixelConfig c = (NeoPixelConfig) config; NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); if (controller == null || np2 == null) { @@ -662,7 +638,7 @@ public void setBrightness(int value) { } brightness = value; np2.neoPixelSetBrightness(getName(), value); - + if (c.autoClear) { purgeTask("clear"); // and start our countdown @@ -718,7 +694,7 @@ public void setPixel(int address, int red, int green, int blue, int white) { * @param delayMs */ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int red, int green, int blue, int white, Integer delayMs) { - NeoPixelConfig c = (NeoPixelConfig)config; + NeoPixelConfig c = (NeoPixelConfig) config; // get and update memory cache PixelSet ps = getPixelSet(matrixName, pixelSetIndex); @@ -746,7 +722,7 @@ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int } np2.neoPixelWriteMatrix(getName(), pixel.flatten()); - + if (c.autoClear) { purgeTask("clear"); // and start our countdown @@ -788,20 +764,6 @@ public void setRed(int red) { this.red = red; } - public void startAnimation() { - startAnimation(currentMatrix); - } - - /** - * handle both user defined, java defined, and controller on board animations - * FIXME - make "settings" separate call - * - * @param name - */ - public void startAnimation(String name) { - animationRunner.start(); - } - public void setColor(int red, int green, int blue) { this.red = red; this.green = green; @@ -814,8 +776,8 @@ public void setColor(int red, int green, int blue) { @Override public void writeMatrix() { - NeoPixelConfig c = (NeoPixelConfig)config; - + NeoPixelConfig c = (NeoPixelConfig) config; + NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); if (controller == null || np2 == null) { error("%s cannot writeMatrix controller not set", getName()); @@ -827,8 +789,6 @@ public void writeMatrix() { // and start our countdown addTaskOneShot(c.idleTimeout, "clear"); } - - } /** @@ -858,7 +818,7 @@ public void playIronman() { @Override public ServiceConfig getConfig() { - NeoPixelConfig config = (NeoPixelConfig)super.getConfig(); + NeoPixelConfig config = (NeoPixelConfig) super.getConfig(); // FIXME - remove local fields in favor of config config.pin = pin; config.pixelCount = pixelCount; @@ -918,6 +878,18 @@ public String onStarted(String name) { return name; } + @Override + public void startService() { + super.startService(); + worker.start(); + } + + public void stopService() { + super.stopService(); + worker.stop(); + // clear() ? + } + public static void main(String[] args) throws InterruptedException { try { @@ -993,9 +965,9 @@ public void setPin(String pin) { error(e); } } - + public boolean setAutoClear(boolean b) { - NeoPixelConfig c = (NeoPixelConfig)config; + NeoPixelConfig c = (NeoPixelConfig) config; c.autoClear = b; return b; } diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index 9c57690e44..c298821bab 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -32,7 +32,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Queue; import java.util.Random; import java.util.Set; import java.util.TimeZone; @@ -69,7 +68,6 @@ import org.myrobotlab.framework.repo.IvyWrapper; import org.myrobotlab.framework.repo.Repo; import org.myrobotlab.framework.repo.ServiceData; -import org.myrobotlab.framework.repo.ServiceDependency; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.AppenderType; import org.myrobotlab.logging.LoggerFactory; @@ -133,7 +131,7 @@ public class Runtime extends Service implements MessageListener, * a registry of all services regardless of which environment they came from - * each must have a unique name */ - static private final Map registry = new TreeMap<>(); + static private final Map registry = new LinkedHashMap<>(); /** * A plan is a request to runtime to change the system. Typically its to ask @@ -436,9 +434,9 @@ synchronized private static Map createServicesFromPlan runtime.error("could not get %s from plan", service); continue; } - sc.state = "CREATING"; + // sc.state = "CREATING"; ServiceInterface si = createService(service, sc.type, null); - sc.state = "CREATED"; + // sc.state = "CREATED"; // process the base listeners/subscription of ServiceConfig si.addConfigListeners(sc); if (si instanceof ConfigurableService) { @@ -1903,13 +1901,6 @@ public synchronized static boolean releaseService(String inName) { if (si.isLocal()) { si.purgeTasks(); si.stopService(); - Plan plan = Runtime.getPlan(); - ServiceConfig sc = plan.get(inName); - if (sc == null) { - log.debug("service config not available for {}", inName); - } else { - sc.state = "STOPPED"; - } } else { if (runtime != null) { runtime.send(name, "releaseService"); @@ -1918,33 +1909,6 @@ public synchronized static boolean releaseService(String inName) { // FOR remote this isn't correct - it should wait for // a message from the other runtime to say that its released unregister(name); - Plan plan = Runtime.getPlan(); - ServiceConfig sc = plan.get(inName); - - if (sc != null) { - sc.state = "RELEASED"; - // FIXME - TODO RELEASE PEERS ! which is any inName.* !!! - - // iterate through peers - // if (sc.autoStartPeers) { - // // get peers from meta data - // MetaData md = MetaData.get(sc.type); - // Map peers = md.getPeers(); - // log.info("auto start peers and {} of type {} has {} peers", inName, - // sc.type, peers.size()); - // // RECURSE ! - if we found peers and autoStartPeers is true - we start - // // all - // // the children up - // for (String peer : peers.keySet()) { - // // get actual Name - // String actualPeerName = getPeerName(peer, sc, peers, inName); - // if (actualPeerName != null && isStarted(actualPeerName) && - // si.autoStartedPeersContains(actualPeerName)) { - // release(actualPeerName); - // } - // } - // } - } return true; } @@ -2576,7 +2540,7 @@ static public void startConfig(String configName) { setConfig(configName); Runtime runtime = Runtime.getInstance(); runtime.processingConfig = true; // multiple inbox threads not available - runtime.invoke("publishStartConfig", configName); + runtime.invoke("publishConfigStarted", configName); RuntimeConfig rtConfig = (RuntimeConfig) runtime.readServiceConfig(runtime.getConfigName(), "runtime"); if (rtConfig == null) { runtime.error("cannot find %s%s%s", runtime.getConfigName(), fs, "runtime.yml"); @@ -2588,6 +2552,7 @@ static public void startConfig(String configName) { // for every service listed in runtime registry - load it // FIXME - regex match on filesystem matches on *.yml for (String service : rtConfig.getRegistry()) { + if ("runtime".equals(service) || Runtime.isStarted(service)) { continue; } @@ -2617,12 +2582,12 @@ static public void startConfig(String configName) { } runtime.processingConfig = false; // multiple inbox threads not available - runtime.invoke("publishFinishedConfig", configName); + runtime.invoke("publishConfigFinished", configName); } - public String publishStartConfig(String configName) { - log.info("publishStartConfig {}", configName); + public String publishConfigStarted(String configName) { + log.info("publishConfigStarted {}", configName); // Make Note: done inline, because the thread actually doing the config // processing // would need to be finished with it before this thread could be invoked @@ -2631,8 +2596,8 @@ public String publishStartConfig(String configName) { return configName; } - public String publishFinishedConfig(String configName) { - log.info("publishFinishedConfig {}", configName); + public String publishConfigFinished(String configName) { + log.info("publishConfigFinished {}", configName); // Make Note: done inline, because the thread actually doing the config // processing // would need to be finished with it before this thread could be invoked @@ -2771,21 +2736,6 @@ public Runtime(String n, String id) { // fist and only time.... runtime = this; repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper"); - - // resolve serviceData MetaTypes for the repo - - for (MetaData metaData : serviceData.getServiceTypes()) { - if (metaData.getSimpleName().equals("OpenCV")) { - log.warn("here"); - } - Set deps = repo.getUnfulfilledDependencies(metaData.getType()); - if (deps.size() == 0) { - metaData.installed = true; - } else { - log.warn("{} not installed", metaData.getSimpleName()); - } - } - } } @@ -3526,7 +3476,7 @@ static public String execute(String... args) { * @return The programs stderr and stdout output */ static public String execute(String program, List args, String workingDir, Map additionalEnv, boolean block) { - log.info("execToString(\"{} {}\")", program, args); + log.debug("execToString(\"{} {}\")", program, args); List command = new ArrayList<>(); command.add(program); @@ -3562,14 +3512,15 @@ static public String execute(String program, List args, String workingDi if (block) { int exitValue = handle.waitFor(); outputBuilder.append("Exit Value: ").append(exitValue); - log.info("Command exited with exit value: {}", exitValue); + log.debug("Command exited with exit value: {}", exitValue); } else { - log.info("Command started"); + log.debug("Command started"); } return outputBuilder.toString(); } catch (IOException e) { log.error("Error executing command", e); + Runtime.getInstance().error(e.getMessage()); return e.getMessage(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -3621,13 +3572,14 @@ public static Double getBatteryLevel() { } else if (platform.isLinux()) { // TODO This is incorrect, will not work when unplugged // and acpitool output is different than expected, - // at least on Ubuntu 22.04 - String ret = Runtime.execute("acpitool"); - int pos0 = ret.indexOf("Charging, "); + // at least on Ubuntu 22.04 - consider oshi library + String ret = Runtime.execute("acpi"); + int pos0 = ret.indexOf("%"); + if (pos0 != -1) { - pos0 = pos0 + 10; - int pos1 = ret.indexOf("%", pos0); - String dble = ret.substring(pos0, pos1).trim(); + int pos1 = ret.lastIndexOf(" ", pos0); + // int pos1 = ret.indexOf("%", pos0); + String dble = ret.substring(pos1, pos0).trim(); try { r = Double.parseDouble(dble); } catch (Exception e) { diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index ecb2d59c4e..4434f3db2d 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -10,82 +10,132 @@ import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition; import org.myrobotlab.service.config.RandomConfig.RandomMessageConfig; +/** + * InMoov2Config - configuration for InMoov2 service + * - this is a "default" configuration + * If its configuration which will directly affect another service the naming + * pattern should be {peerName}{propertyName} + * e.g. neoPixelErrorRed + * + * FIXME make a color map that can be overridden + * + * @author GroG + * + */ public class InMoov2Config extends ServiceConfig { - public int analogPinFromSoundCard = 53; - - public int audioPollsBySeconds = 2; - - public boolean audioSignalProcessing=false; - - public boolean batteryInSystem = false; - - public boolean customSound=false; + /** + * When the healthCheck is operating, it will check the battery level. + * If the battery level is < 5% it will publishFlash with red at regular interval + */ + public boolean batteryLevelCheck = false; + + // public boolean customSound = false; + + /** + * First Initialization occurs when the user starts the InMoov2 service for the + * very first time, and ProgramAB can process learning about the user + */ + public boolean firstInit = true; public boolean forceMicroOnIfSleeping = true; - public boolean healthCheckActivated = false; + /** + * flashes if error has occurred - requires heartbeat + */ + public boolean healthCheckFlash = true; - public int healthCheckTimerMs = 60000; + /** + * Single heartbeat to drive InMoov2 .. it can check status, healthbeat, + * and fire events to the FSM. + * Checks battery level and sends a heartbeat flash on publishHeartbeat + * and onHeartbeat at a regular interval + */ + public boolean heartbeat = true; + + /** + * flashes the neopixel every time a health check is preformed. + * green == good + * red == battery < 5% + */ + public boolean heartbeatFlash = false; - public boolean heartbeat = false; + /** + * interval heath check processes in milliseconds + */ + public long heartbeatInterval = 3000; - /** - * idle time measures the time the fsm is in an idle state + * loads all python gesture files in the gesture directory */ - public boolean idleTimer = true; - public boolean loadGestures = true; + /** + * executes all scripts in the init directory on startup + */ + public boolean loadInitScripts = true; + /** * default to null - allow the OS to set it, unless explicilty set */ public String locale = null; // = "en-US"; - public boolean neoPixelBootGreen=true; + public boolean neoPixelBootGreen = true; public boolean neoPixelDownloadBlue = true; public boolean neoPixelErrorRed = true; - + public boolean neoPixelFlashWhenSpeaking = true; - - public boolean openCVFaceRecognizerActivated=true; - - public boolean openCVFlipPicture=false; - + + public boolean openCVFaceRecognizerActivated = true; + + public boolean openCVFlipPicture = false; + public boolean pirEnableTracking = false; - + /** - * play pir sounds when pir switching states - * sound located in data/InMoov2/sounds/pir-activated.mp3 - * sound located in data/InMoov2/sounds/pir-deactivated.mp3 + * play pir sounds when pir switching states sound located in + * data/InMoov2/sounds/pir-activated.mp3 sound located in + * data/InMoov2/sounds/pir-deactivated.mp3 */ public boolean pirPlaySounds = true; - + public boolean pirWakeUp = true; - + public boolean robotCanMoveHeadWhileSpeaking = true; - - + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest */ public double shutdownStartupSpeed = 50; - + /** - * Sleep 5 minutes after last presence detected + * Sleep 5 minutes after last presence detected */ - public int sleepTimeoutMs=300000; - + public int sleepTimeoutMs = 300000; + public boolean startupSound = true; - public int trackingTimeoutMs=10000; - + /** + * Interval in seconds for a idle state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to idle. Heartbeat will fire the event. + */ + public int stateIdleInterval = 120; + + /** + * Interval in seconds for a random state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to random. Heartbeat will fire the event. + */ + public int stateRandomInterval = 120; + + public int trackingTimeoutMs = 10000; + public String unlockInsult = "forgive me"; - + public boolean virtual = false; public InMoov2Config() { @@ -113,6 +163,8 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false); addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false); addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false); + // a first ! + addDefaultPeerConfig(plan, name, "mouth.audioFile", "AudioFile", false); addDefaultPeerConfig(plan, name, "mouthControl", "MouthControl", false); addDefaultPeerConfig(plan, name, "neoPixel", "NeoPixel", false); addDefaultPeerConfig(plan, name, "opencv", "OpenCV", false); @@ -132,7 +184,7 @@ public Plan getDefault(Plan plan, String name) { MouthControlConfig mouthControl = (MouthControlConfig) plan.get(getPeerName("mouthControl")); - // setup name references to different services + // setup name references to different services FIXME getPeerName("head").getPeerName("jaw") mouthControl.jaw = name + ".head.jaw"; String i01Name = name; int index = name.indexOf("."); @@ -156,9 +208,9 @@ public Plan getDefault(Plan plan, String name) { } } } - + chatBot.currentUserName = "human"; - + // chatBot.textListeners = new String[] { name + ".htmlFilter" }; if (chatBot.listeners == null) { chatBot.listeners = new ArrayList<>(); @@ -181,11 +233,13 @@ public Plan getDefault(Plan plan, String name) { // == Peer - ear ============================= // setup name references to different services WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear")); - ear.listeners = new ArrayList<>(); + ear.listeners = new ArrayList<>(); ear.listeners.add(new Listener("publishText", name + ".chatBot", "onText")); ear.listening = true; // remove, should only need ServiceConfig.listeners - ear.textListeners = new String[]{name + ".chatBot"}; + ear.textListeners = new String[] { name + ".chatBot" }; + + JMonkeyEngineConfig simulator = (JMonkeyEngineConfig) plan.get(getPeerName("simulator")); @@ -254,24 +308,28 @@ public Plan getDefault(Plan plan, String name) { simulator.cameraLookAt = name + ".torso.lowStom"; FiniteStateMachineConfig fsm = (FiniteStateMachineConfig) plan.get(getPeerName("fsm")); - // TODO - events easily gotten from InMoov data ?? auto callbacks in python if exists ? + // TODO - events easily gotten from InMoov data ?? auto callbacks in python + // if exists ? + fsm.listeners = new ArrayList<>(); fsm.current = "boot"; - fsm.transitions.add(new Transition("boot", "configStarted", "applyingConfig")); - fsm.transitions.add(new Transition("applyingConfig", "getUserInfo", "getUserInfo")); - fsm.transitions.add(new Transition("applyingConfig", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("applyingConfig", "wake", "awake")); - fsm.transitions.add(new Transition("getUserInfo", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); - fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); + fsm.transitions.add(new Transition("boot", "wake", "wake")); + fsm.transitions.add(new Transition("wake", "idle", "idle")); + fsm.transitions.add(new Transition("firstInit", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "random", "random")); + fsm.transitions.add(new Transition("random", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "sleep", "sleep")); + fsm.transitions.add(new Transition("idle", "powerDown", "powerDown")); + fsm.transitions.add(new Transition("wake", "firstInit", "firstInit")); + // powerDown to shutdown +// fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); +// fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); - - PirConfig pir = (PirConfig) plan.get(getPeerName("pir")); pir.pin = "23"; pir.controller = name + ".left"; pir.listeners = new ArrayList<>(); pir.listeners.add(new Listener("publishPirOn", name, "onPirOn")); - + // == Peer - random ============================= RandomConfig random = (RandomConfig) plan.get(getPeerName("random")); random.enabled = false; @@ -381,16 +439,47 @@ public Plan getDefault(Plan plan, String name) { plan.remove(name + ".eyeTracking.controller.serial"); plan.remove(name + ".eyeTracking.cv"); + // LOOPBACK PUBLISHING - ITS A GREAT WAY TO SUPPORT + // EXTENSIBLE AND OVERRIDABLE BEHAVIORS + // inmoov2 default listeners listeners = new ArrayList<>(); // FIXME - should be getPeerName("neoPixel") - listeners.add(new Listener("publishFlash", name + ".neoPixel", "onLedDisplay")); - - listeners.add(new Listener("publishEvent", name + ".fsm")); - // remove the auto-added starts in the plan's runtime RuntimConfig.registry - plan.removeStartsWith(name + "."); + // loopbacks allow user to override or extend with python + listeners.add(new Listener("publishBoot", name)); + listeners.add(new Listener("publishHeartbeat", name)); + listeners.add(new Listener("publishConfigFinished", name)); + listeners.add(new Listener("publishNewState", name)); + +// listeners.add(new Listener("publishPowerUp", name)); +// listeners.add(new Listener("publishPowerDown", name)); +// listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + // service --to--> InMoov2 + AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); + mouth_audioFile.listeners = new ArrayList<>(); + mouth_audioFile.listeners.add(new Listener("publishPeak", name)); + fsm.listeners.add(new Listener("publishNewState", name, "publishNewState")); +// mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); +// mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + // InMoov2 --to--> service + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay")); + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + + + // remove the auto-added starts in the plan's runtime RuntimConfig.registry + // plan.removeStartsWith(name + "."); + // rtConfig.add(name); // <-- adding i01 / not needed return plan; diff --git a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java index c2ad476697..e7fbb17cbe 100644 --- a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java +++ b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java @@ -14,7 +14,7 @@ public class NeoPixelConfig extends ServiceConfig { public Integer brightness = 255; public boolean fill = false; // auto clears flashes - public boolean autoClear = true; + public boolean autoClear = false; public int idleTimeout = 1000; } diff --git a/src/main/java/org/myrobotlab/service/config/ServiceConfig.java b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java index 59e8f9ff9a..dd28a48645 100755 --- a/src/main/java/org/myrobotlab/service/config/ServiceConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ServiceConfig.java @@ -73,12 +73,7 @@ public String toString() { * simple type name of service defined for this config */ public String type; - - // FIXME - change to enum ! - // FIXME - remove this - its not used - // heh non transient makes it easy to debug ! - transient public String state = "INIT"; // INIT | LOADED | CREATED | STARTED | - // STOPPED | RELEASED + public String getPath(String name, String peerKey) { if (name == null) { diff --git a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java index a49520c1e4..c84c4dca65 100644 --- a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java +++ b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java @@ -8,6 +8,7 @@ public class SpeechSynthesisConfig extends ServiceConfig { public boolean mute = false; public boolean blocking = false; + @Deprecated /* :( ... this is already in listeners ! */ public String[] speechRecognizers; public Map substitutions; public String voice; diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java index 92ce88ba38..3658782913 100644 --- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java +++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java @@ -1,28 +1,65 @@ package org.myrobotlab.service.data; + /** - * Class to publish to specify details on how to display an led or a group of leds. - * There is a need to "flash" LEDs in order to signal some event. This is the - * beginning of an easy way to publish a message to do that. + * Class to publish to specify details on how to display an led or a group of + * leds. There is a need to "flash" LEDs in order to signal some event. This is + * the beginning of an easy way to publish a message to do that. * * @author GroG * */ public class LedDisplayData { - public String action; // fill | flash | play animation | stop | clear - public int red; - public int green; - public int blue; - //public int white?; - - /** - * number of flashes - */ - public int count = 5; - /** - * interval of flash in ms - */ - public long interval = 500; - - + public String action; // fill | flash | play animation | stop | clear + + public int red; + + public int green; + + public int blue; + + // public int white?; + + /** + * number of flashes + */ + public int count = 1; + + /** + * interval of flash on in ms + */ + public long timeOn = 500; + + /** + * interval of flas off in ms + */ + public long timeOff = 500; + + public LedDisplayData() { + } + + public LedDisplayData(int red, int green, int blue, int count, int timeOn, int timeOff) { + this.red = red; + this.green = green; + this.blue = blue; + this.count = count; + this.timeOn = timeOn; + this.timeOff = timeOff; + } + + + public LedDisplayData(String hexColor, int count, int timeOn, int timeOff) { + + // remove "#" or "0x" prefix if present + hexColor = hexColor.replace("#", "").replace("0x", ""); + + this.count = count; + this.timeOn = timeOn; + this.timeOff = timeOff; + this.red = Integer.parseInt(hexColor.substring(0, 2), 16); + this.green = Integer.parseInt(hexColor.substring(2, 4), 16); + this.blue = Integer.parseInt(hexColor.substring(4, 6), 16); + + } + } diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java index 003b956f34..33e9b416ac 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioControl.java @@ -2,6 +2,7 @@ public interface AudioControl { + // FIXME - should be onVolume(volume) public void setVolume(double volume); public double getVolume(); @@ -10,7 +11,7 @@ public interface AudioControl { * plays an audiofile - is a listener function for publishAudioFile * @param file */ - public void onPlayAudioFile(String file); + public void onPlayAudioFile(String dir); /** * must be a directory, will play one of the audio files within that directory @@ -18,7 +19,7 @@ public interface AudioControl { */ public void onPlayRandomAudioFile(String dir); - // pause - // resume - // interrupt ? + // onPause + // onResume + // onInterrupt ? } diff --git a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js index 05f67ef4ea..03f830ed78 100644 --- a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js @@ -11,7 +11,7 @@ angular.module('mrlapp.service.NeoPixelGui', []).controller('NeoPixelGuiCtrl', [ $scope.pins = [] $scope.speeds = [] $scope.types = ['RGB', 'RGBW'] - $scope.animations = ['No animation', 'Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman', 'equalizer'] + $scope.animations = ['Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman'] $scope.pixelCount = null // set pixel position diff --git a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js index 09eab9b6f6..e54e06e36b 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js @@ -361,7 +361,9 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ } $scope.saveConfig = function() { - $scope.service.selectedOption = 'current' + $scope.service.includePeers = false + $scope.service.selectedOption = "current" + var modalInstance = $uibModal.open({ templateUrl: 'saveConfig.html', scope: $scope, diff --git a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html index 0cda4d2622..d3cef04291 100644 --- a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html @@ -10,7 +10,7 @@

{{address}} {{color}}

pixel count   - + From 5a558122ba948676cd866b06c14b0d947f86a6bc Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 3 Sep 2023 12:06:08 -0700 Subject: [PATCH 020/232] rich change state object --- .../service/FiniteStateMachine.java | 27 +++- .../java/org/myrobotlab/service/InMoov2.java | 146 ++++++++++-------- .../service/config/InMoov2Config.java | 25 ++- 3 files changed, 121 insertions(+), 77 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index f4d56d168e..280b31b9db 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -63,6 +63,17 @@ 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; + } + } private static Transition toFsmTransition(StateTransition state) { Transition transition = new Transition(); @@ -179,7 +190,7 @@ 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) { @@ -222,21 +233,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.error("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 @@ -404,7 +415,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/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 2476bbbeef..36aa88718c 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -30,6 +30,7 @@ import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; +import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -141,6 +142,11 @@ public static void main(String[] args) { protected Map ledDisplayMap = new TreeMap<>(); + /** + * map of events or states to sounds + */ + protected Map customSoundMap = new TreeMap<>(); + protected List errors = new ArrayList<>(); /** @@ -198,7 +204,6 @@ public InMoov2(String n, String id) { // consequence it this will need maintenance if there are new InMoov2 java // state handlers stateDefaults.add("wake"); - stateDefaults.add("idle"); stateDefaults.add("firstInit"); stateDefaults.add("idle"); stateDefaults.add("random"); @@ -215,6 +220,16 @@ public InMoov2(String n, String id) { ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); ledDisplayMap.put("pirOn", new LedDisplayData(80, 200, 90, 3, 100, 30)); ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); + + customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); + customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/sense.wav")); + customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); + customSoundMap.put("idle", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/start.wav")); + customSoundMap.put("random", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/reveal.wav")); + customSoundMap.put("sleep", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/back.wav")); + customSoundMap.put("powerDown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("shutdown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/marimba.wav")); + } /** @@ -716,7 +731,7 @@ public void loadGestures() { * @return true/false */ public boolean loadGestures(String directory) { - invoke("publishEvent", "LOAD GESTURES"); + systemEvent("LOAD GESTURES"); // iterate over each of the python files in the directory // and load them into the python interpreter. @@ -771,7 +786,7 @@ public void loadScripts(String directory) throws IOException { File dir = new File(directory); if (!dir.exists() || !dir.isDirectory()) { - invoke("publishEvent", "LOAD SCRIPTS ERROR"); + systemEvent("LOAD SCRIPTS ERROR"); return; } @@ -788,6 +803,7 @@ public boolean accept(File dir, String name) { for (File file : files) { Python p = (Python) Runtime.start("python", "Python"); if (p != null) { + // FIXME error("x") when an error occurs p.execFile(file.getAbsolutePath()); } } @@ -924,7 +940,7 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { public PredicateEvent onChangePredicate(PredicateEvent event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { - invoke("publishEvent", String.format("TOPIC CHANGED TO %s", event.value)); + systemEvent("TOPIC CHANGED TO %s", event.value); } // depending on configuration .... // call python ? @@ -1042,7 +1058,6 @@ public void onJointAngles(Map angleMap) { public void onJoystickInput(JoystickData input) throws Exception { // TODO timer ? to test and not send an event // switches to manual control ? - invoke("publishEvent", "joystick"); } public void onMoveHead(Map map) { @@ -1109,14 +1124,21 @@ public void onMoveTorso(Map map) { * @param state * @return */ - public String onNewState(String state) { - log.error("onNewState {}", state); + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange state) { + log.error("onStateChange {}", state); + + // if ("boot".equals(state.last) && config.stateBootIsMute && + // getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis) getPeer("mouth"); + // mouth.setMute(wasMutedBeforeBoot); + // } - invoke("publishEvent", String.format("ON STATE %s", state)); + systemEvent("ON STATE %s", state.current); // TODO - only a few InMoov2 state defaults will be called here - if (stateDefaults.contains(state)) { - invoke(state); + if (stateDefaults.contains(state.current)) { + invoke(state.current); } // FIXME add topic changes to AIML here ! @@ -1125,7 +1147,7 @@ public String onNewState(String state) { // put configurable filter here ! // state substitutions ? - // let python subscribe directly to fsm.publishNewState + // let python subscribe directly to fsm.publishStateChange // if python && configured to do python inmoov2 library callbacks // do a callback ... default NOOPs should be in library @@ -1146,6 +1168,7 @@ public OpenCVData onOpenCVData(OpenCVData data) { /** * onPeak volume callback TODO - maybe make it variable with volume ? + * * @param volume */ public void onPeak(double volume) { @@ -1171,14 +1194,8 @@ public void onPirOn() { invoke("publishFlash", pirOn); } } - // pirOn event vs wake event - // FIXME - this is state related - String topic = chatBot.getPredicate("topic"); - // if sleeping then publishe a wake event - if ("sleep".equals(topic)) { - invoke("publishEvent", "wake"); - } + fsm.fire("wake"); } public void powerDown() { @@ -1218,9 +1235,9 @@ public boolean onSense(boolean b) { // setEvent("pir-sense-on" .. also sets it in config ? // config.handledEvents["pir-sense-on"] if (b) { - invoke("publishEvent", "PIR ON"); + systemEvent("PIR ON"); } else { - invoke("publishEvent", "PIR OFF"); + systemEvent("PIR OFF"); } return b; } @@ -1302,7 +1319,17 @@ public String publishConfigFinished(String configName) { public List publishConfigList() { return configList; } + + public String systemEvent(String eventMsg) { + invoke("publishSystemEvent", eventMsg); + return eventMsg; + } + public String systemEvent(String format, Object ...ags) { + String eventMsg = String.format(format, ags); + return systemEvent(eventMsg); + } + /** * event publisher for the fsm - although other services potentially can * consume and filter this event channel @@ -1310,7 +1337,8 @@ public List publishConfigList() { * @param event * @return */ - public String publishEvent(String event) { + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in aiml is wildcard ... duh return String.format("SYSTEM_EVENT %s", event); } @@ -1371,8 +1399,8 @@ public Map publishMoveTorso(Map map) { return map; } - public String publishNewState(String state) { - log.info("publishNewState {}", state); + public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { + log.info("publishStateChange {}", state); return state; } @@ -1412,7 +1440,7 @@ public String publishText(String text) { public void releasePeer(String peerKey) { super.releasePeer(peerKey); if (peerKey != null) { - invoke("publishEvent", "STOPPED " + peerKey); + systemEvent("STOPPED %s", peerKey); } } @@ -1696,29 +1724,8 @@ public void speakBlocking(String format, Object... args) { } } - public void startAll() throws Exception { - startAll(null, null); - } - - public void startAll(String leftPort, String rightPort) throws Exception { - startMouth(); - startChatBot(); - - // startHeadTracking(); - // startEyesTracking(); - // startOpenCV(); - startEar(); - - startServos(); - // startMouthControl(head.jaw, mouth); - - speakBlocking(get("STARTINGSEQUENCE")); - } - - public void startBrain() { - startChatBot(); - } + @Deprecated /*This needs to be removed !*/ public ProgramAB startChatBot() { try { @@ -1767,10 +1774,12 @@ public ProgramAB startChatBot() { if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown") || chatBot.getPredicate("default", "firstinit").equals("started")) { chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "FIRST INIT"); + // probably not necessary - state change events should be enough + systemEvent("FIRST INIT"); } else { chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "WAKE UP"); + // probably not necessary - state change events should be enough + systemEvent("WAKE UP"); } } catch (Exception e) { speak("could not load chatBot"); @@ -1909,6 +1918,8 @@ public void publishBoot() { log.info("publishBoot"); } + boolean wasMutedBeforeBoot = false; + /** * At boot all services specified through configuration have started, or if no * configuration has started minimally the InMoov2 service has started. During @@ -1961,6 +1972,7 @@ synchronized public void onBoot() { error(e); } + // FIXME - find good way of running an animation "through" a state if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); if (neoPixel != null) { @@ -1974,20 +1986,21 @@ synchronized public void onBoot() { ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); } - if (bootedConfig != null) { - // configuration was processed before booting - invoke("publishEvent", "CONFIG STARTED " + bootedConfig); - } - - // publish config started + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG STARTED %s", bootedConfig); + } - for (String peerKey : peersStarted) { - invoke("publishEvent", "STARTED " + peerKey); - } + for (String peerKey : peersStarted) { + systemEvent("STARTED %s", peerKey); + } - if (bootedConfig != null) { - // configuration was processed before booting - invoke("publishEvent", "CONFIG LOADED " + bootedConfig); + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } } // FIXME - important to do invoke & fsm needs to be consistent order @@ -2010,6 +2023,12 @@ synchronized public void onBoot() { fsm.fire("wake"); + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + } /** @@ -2101,6 +2120,11 @@ public void wake() { public void firstInit() { log.info("firstInit"); + // cheap way to prevent race condition + // of "wake" firing a state change .. which will spawn + // a system event of FIRST_INIT that will answer this + // question ... + sleep(2000); ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); if (chatBot != null) { chatBot.getResponse("FIRST_INIT"); @@ -2165,7 +2189,7 @@ public void systemCheck() { Platform platform = Runtime.getPlatform(); setPredicate("system version", platform.getVersion()); // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? } // FIXME - if this is really desired it will drive local references for all diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 4434f3db2d..ca547d15f6 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -30,13 +30,11 @@ public class InMoov2Config extends ServiceConfig { */ public boolean batteryLevelCheck = false; - // public boolean customSound = false; - /** - * First Initialization occurs when the user starts the InMoov2 service for the - * very first time, and ProgramAB can process learning about the user + * enable custom sound map for state changes */ - public boolean firstInit = true; + public boolean customSound = false; + public boolean forceMicroOnIfSleeping = true; @@ -104,7 +102,7 @@ public class InMoov2Config extends ServiceConfig { public boolean pirWakeUp = true; public boolean robotCanMoveHeadWhileSpeaking = true; - + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest @@ -118,6 +116,17 @@ public class InMoov2Config extends ServiceConfig { public boolean startupSound = true; + /** + * Determines if InMoov2 publish system events during boot state + */ + public boolean systemEventsOnBoot = false; + + /** + * + */ + public boolean stateChangeIsMute = true; + + /** * Interval in seconds for a idle state event to fire off. * If the fsm is in a state which will allow transitioning, the InMoov2 @@ -450,7 +459,7 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishBoot", name)); listeners.add(new Listener("publishHeartbeat", name)); listeners.add(new Listener("publishConfigFinished", name)); - listeners.add(new Listener("publishNewState", name)); + listeners.add(new Listener("publishStateChange", name)); // listeners.add(new Listener("publishPowerUp", name)); // listeners.add(new Listener("publishPowerDown", name)); @@ -467,7 +476,7 @@ public Plan getDefault(Plan plan, String name) { AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); mouth_audioFile.listeners = new ArrayList<>(); mouth_audioFile.listeners.add(new Listener("publishPeak", name)); - fsm.listeners.add(new Listener("publishNewState", name, "publishNewState")); + fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); From 6d60fdcbf23cc02f938358e189ff9110cac6b3e4 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 3 Sep 2023 14:54:30 -0700 Subject: [PATCH 021/232] cleaned up onStateChange --- .../java/org/myrobotlab/service/InMoov2.java | 173 +++++++++--------- .../java/org/myrobotlab/service/NeoPixel.java | 6 +- 2 files changed, 95 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 36aa88718c..3a9de36b24 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -30,7 +30,6 @@ import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; -import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -188,9 +187,9 @@ public static void main(String[] args) { protected String voiceSelected; /** - * prevents being booted more than once + * Prevents actions or events from happening when InMoov2 is first booted */ - private boolean booted = false; + private boolean hasBooted = false; protected List peersStarted = new ArrayList<>(); @@ -222,7 +221,7 @@ public InMoov2(String n, String id) { ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); - customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/sense.wav")); + customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); customSoundMap.put("idle", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/start.wav")); customSoundMap.put("random", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/reveal.wav")); @@ -985,58 +984,64 @@ public void onGestureStatus(Status status) { unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + /** + * A generalized recurring event which can preform checks and various other + * methods or tasks. Heartbeats will not start until after boot stage. + */ public void onHeartbeat() { - // heartbeats can start before config is - // done processing - so the following should - // not be dependent on config - - if (config.heartbeatFlash) { - if (ledDisplayMap.get("heartbeat") != null) { - LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); - invoke("publishFlash", heartbeat); + try { + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + + if (!hasBooted) { + log.info("boot hasn't completed, will not process heartbeat"); + return; } - } - if (config.batteryLevelCheck) { - double batteryLevel = Runtime.getBatteryLevel(); - invoke("publishBatteryLevel", batteryLevel); - // FIXME - thresholding should always have old value or state - // so we don't pump endless errors - if (batteryLevel < 5) { - error("battery level < 5 percent"); - } else if (batteryLevel < 10) { - warn("battery level < 10 percent"); + if (config.batteryLevelCheck) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } } - } - // flash error until errors are cleared - if (config.healthCheckFlash && errors.size() > 0) { - if (ledDisplayMap.containsKey("error")) { - invoke("publishFlash", ledDisplayMap.get("error")); + // flash error until errors are cleared + if (config.healthCheckFlash) { + if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { + invoke("publishFlash", ledDisplayMap.get("error")); + } else if (ledDisplayMap.containsKey("heartbeat")) { + LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); + invoke("publishFlash", heartbeat); + } } - } - // interval event firing ... - // sequence of these makes a difference and is hardcode fyi + Long lastActivityTime = getLastActivityTime(); - // if there is activity in the idle time window - reset idle time to current - // time - // so idle event will not fire until at least inverval after activity - Long lastActivityTime = getLastActivityTime(); + if (lastActivityTime != null && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } - if (lastActivityTime != null && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { - stateLastIdleTime = lastActivityTime; - } + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fsm.fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } - if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { - fsm.fire("idle"); - stateLastIdleTime = System.currentTimeMillis(); - } + // interval event firing + if (System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + fsm.fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } - // interval event firing - if (System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { - fsm.fire("random"); - stateLastRandomTime = System.currentTimeMillis(); + } catch (Exception e) { + error(e); } } @@ -1121,44 +1126,46 @@ public void onMoveTorso(Map map) { * Depending on config: * * - * @param state + * @param stateChange * @return */ - public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange state) { - log.error("onStateChange {}", state); + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { + try { + log.error("onStateChange {}", stateChange); - // if ("boot".equals(state.last) && config.stateBootIsMute && - // getPeer("mouth") != null) { - // AbstractSpeechSynthesis mouth = - // (AbstractSpeechSynthesis) getPeer("mouth"); - // mouth.setMute(wasMutedBeforeBoot); - // } + String state = stateChange.current; + systemEvent("ON STATE %s", state); - systemEvent("ON STATE %s", state.current); + if (config.customSounds && customSoundMap.containsKey(state)) { + invoke("publishPlayAudioFile", customSoundMap.get(state)); + } - // TODO - only a few InMoov2 state defaults will be called here - if (stateDefaults.contains(state.current)) { - invoke(state.current); - } + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(state)) { + invoke(state); + } - // FIXME add topic changes to AIML here ! - // FIXME add clallbacks to inmmoov2 library + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library - // put configurable filter here ! + // put configurable filter here ! - // state substitutions ? - // let python subscribe directly to fsm.publishStateChange + // state substitutions ? + // let python subscribe directly to fsm.publishStateChange - // if python && configured to do python inmoov2 library callbacks - // do a callback ... default NOOPs should be in library + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library - // if - // invoke(state); - // depending on configuration .... - // call python ? - // fire fsm events ? - // do defaults ? - return state; + // if + // invoke(state); + // depending on configuration .... + // call python ? + // fire fsm events ? + // do defaults ? + } catch (Exception e) { + error(e); + } + return stateChange; } public OpenCVData onOpenCVData(OpenCVData data) { @@ -1319,17 +1326,17 @@ public String publishConfigFinished(String configName) { public List publishConfigList() { return configList; } - + public String systemEvent(String eventMsg) { invoke("publishSystemEvent", eventMsg); - return eventMsg; + return eventMsg; } - public String systemEvent(String format, Object ...ags) { + public String systemEvent(String format, Object... ags) { String eventMsg = String.format(format, ags); - return systemEvent(eventMsg); + return systemEvent(eventMsg); } - + /** * event publisher for the fsm - although other services potentially can * consume and filter this event channel @@ -1338,7 +1345,8 @@ public String systemEvent(String format, Object ...ags) { * @return */ public String publishSystemEvent(String event) { - // well, it turned out underscore was a goofy selection, as underscore in aiml is wildcard ... duh + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh return String.format("SYSTEM_EVENT %s", event); } @@ -1724,8 +1732,7 @@ public void speakBlocking(String format, Object... args) { } } - - @Deprecated /*This needs to be removed !*/ + @Deprecated /* This needs to be removed ! */ public ProgramAB startChatBot() { try { @@ -1934,11 +1941,10 @@ public void publishBoot() { synchronized public void onBoot() { // thinking you shouldn't "boot" twice ? - if (booted) { + if (hasBooted) { log.warn("will not boot again"); return; } - booted = true; List services = Runtime.getServices(); for (ServiceInterface si : services) { @@ -2029,6 +2035,7 @@ synchronized public void onBoot() { // mouth.setMute(wasMute); // } + hasBooted = true; } /** diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 05a2106a5d..d9fa5e51cb 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -386,7 +386,11 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { } public void onLedDisplay(LedDisplayData data) { - displayQueue.add(data); + try { + displayQueue.add(data); + } catch(IllegalStateException e) { + log.info("queue full"); + } } public void flash() { From 5ef82fa1f61850c9649c5cf5e1678aee7eb51f84 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 7 Sep 2023 08:06:07 -0700 Subject: [PATCH 022/232] py4j.py a wip but other stuff worky --- .../framework/interfaces/JsonInvoker.java | 22 ++++ .../framework/interfaces/JsonSender.java | 12 ++ .../framework/interfaces/MessageSender.java | 2 +- .../interfaces/SimpleMessageSender.java | 9 ++ .../java/org/myrobotlab/service/Gpt3.java | 6 +- .../java/org/myrobotlab/service/InMoov2.java | 14 +- .../java/org/myrobotlab/service/Py4j.java | 62 +++++++-- .../service/config/InMoov2Config.java | 13 +- .../service/interfaces/Executor.java | 5 +- src/main/resources/resource/Py4j/Py4j.py | 123 +++++++++++++++++- .../app/service/js/FiniteStateMachineGui.js | 6 +- .../WebGui/app/service/tab-header.html | 2 +- 12 files changed, 242 insertions(+), 34 deletions(-) create mode 100644 src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java create mode 100644 src/main/java/org/myrobotlab/framework/interfaces/JsonSender.java create mode 100644 src/main/java/org/myrobotlab/framework/interfaces/SimpleMessageSender.java diff --git a/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java b/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java new file mode 100644 index 0000000000..76b4628079 --- /dev/null +++ b/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java @@ -0,0 +1,22 @@ +package org.myrobotlab.framework.interfaces; + +import org.myrobotlab.framework.Message; + +public interface JsonInvoker { + + /** + * No parameter method + * @param method + * @return + */ + public Object invoke(String method); + + /** + * Encoded parameters as a JSON String (encoded once!) + * @param method + * @param encodedParameters + * @return + */ + public Object invoke(String method, String encodedParameters); + +} 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/Gpt3.java b/src/main/java/org/myrobotlab/service/Gpt3.java index 19da929f49..67ed717639 100644 --- a/src/main/java/org/myrobotlab/service/Gpt3.java +++ b/src/main/java/org/myrobotlab/service/Gpt3.java @@ -119,10 +119,7 @@ public Response getResponse(String text) { @SuppressWarnings({ "unchecked", "rawtypes" }) Map textObject = (Map) choices.get(0); responseText = (String) textObject.get("text"); - if (responseText != null) { - // /completions - invoke("publishText", responseText); - } else { + if (responseText == null) { // /chat/completions @SuppressWarnings({ "unchecked", "rawtypes" }) Map content = (Map)textObject.get("message"); @@ -156,6 +153,7 @@ public Response getResponse(String text) { if (responseText != null && responseText.length() > 0) { invoke("publishUtterance", utterance); invoke("publishResponse", response); + invoke("publishText", responseText); } return response; diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 3a9de36b24..908806f2a1 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1134,7 +1134,15 @@ public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChan log.error("onStateChange {}", stateChange); String state = stateChange.current; - systemEvent("ON STATE %s", state); + + // getPeer("py4j") ? + // Py4j py4j +// String code = getName()".onStateChange(" +// invoke("publishPython", "onStateChange", stateChange ); + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", state); + } if (config.customSounds && customSoundMap.containsKey(state)) { invoke("publishPlayAudioFile", customSoundMap.get(state)); @@ -1167,6 +1175,10 @@ public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChan } return stateChange; } + +// public Message publishPython(String method, Object...data) { +// return Message.createMessage(getName(), getName(), method, data); +// } public OpenCVData onOpenCVData(OpenCVData data) { // FIXME - publish event with or without data ? String file reference diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java index fd2755a733..dd36bf505b 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"); } @@ -586,7 +590,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/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index ca547d15f6..6d4354f121 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -33,7 +33,7 @@ public class InMoov2Config extends ServiceConfig { /** * enable custom sound map for state changes */ - public boolean customSound = false; + public boolean customSounds = false; public boolean forceMicroOnIfSleeping = true; @@ -121,6 +121,11 @@ public class InMoov2Config extends ServiceConfig { */ public boolean systemEventsOnBoot = false; + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; + /** * */ @@ -225,6 +230,12 @@ public Plan getDefault(Plan plan, String name) { chatBot.listeners = new ArrayList<>(); } chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + + + ProgramABConfig gpt3 = (ProgramABConfig) plan.get(getPeerName("gpt3")); + gpt3.listeners = new ArrayList<>(); + gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + HtmlFilterConfig htmlFilter = (HtmlFilterConfig) plan.get(getPeerName("htmlFilter")); // htmlFilter.textListeners = new String[] { name + ".mouth" }; diff --git a/src/main/java/org/myrobotlab/service/interfaces/Executor.java b/src/main/java/org/myrobotlab/service/interfaces/Executor.java index e1566c015a..85ebdb61d1 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/Executor.java +++ b/src/main/java/org/myrobotlab/service/interfaces/Executor.java @@ -1,6 +1,7 @@ package org.myrobotlab.service.interfaces; -import org.myrobotlab.framework.interfaces.Invoker; +import org.myrobotlab.framework.interfaces.JsonInvoker; +import org.myrobotlab.framework.interfaces.JsonSender; /** * Interface to a Executor - currently only utilized by Py4j to @@ -10,7 +11,7 @@ * @author GroG * */ -public interface Executor extends Invoker { +public interface Executor extends JsonInvoker, JsonSender { /** * exec in Python - executes arbitrary code 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/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html index 4abedeb490..4557717bdc 100644 --- a/src/main/resources/resource/WebGui/app/service/tab-header.html +++ b/src/main/resources/resource/WebGui/app/service/tab-header.html @@ -55,7 +55,7 @@
  • - +   subscriptions From 832880e0a102d0cd4be09fab33c6cc8083914fb2 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 7 Sep 2023 08:49:07 -0700 Subject: [PATCH 023/232] gpt3 config --- src/main/java/org/myrobotlab/service/config/InMoov2Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 6d4354f121..7c188e95d5 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -232,7 +232,7 @@ public Plan getDefault(Plan plan, String name) { chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); - ProgramABConfig gpt3 = (ProgramABConfig) plan.get(getPeerName("gpt3")); + Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3")); gpt3.listeners = new ArrayList<>(); gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); From 3cd480c4b22c73096c41d44850b8241e3695c49a Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 9 Sep 2023 19:12:32 -0700 Subject: [PATCH 024/232] readme update --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 37d18e96a6..7602ad92d9 100644 --- a/README.md +++ b/README.md @@ -101,24 +101,24 @@ 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 +InMoov2 UI if the repos are submoduled under +src/main/resources/resource/InMoov2, +src/main/resources/resource/ProgramAB ```yml !!org.myrobotlab.service.config.WebGuiConfig -autoStartBrowser: true +autoStartBrowser: false enableMdns: false listeners: null peers: null port: 8888 resources: # these are the only two in usual runtime -- ./resource/WebGui/app -- ./resource + # - ./resource/WebGui/app + # - ./resource # the rest are useful when doing dev -- ../InMoov2/resource/WebGui/app - ./src/main/resources/resource/WebGui/app -- ./src/main/resources/resource/WebGui +- ./src/main/resources/resource/InMoov2/peers/WebGui/app - ./src/main/resources/resource -- ./src/main/resources type: WebGui ``` ```yml @@ -134,7 +134,7 @@ registry: - security - webgui - python -resource: src/main/resources/resource +resource: ./src/main/resources/resource type: Runtime virtual: false ``` From 13c61a6621e74d70ebfcf129ec78cebbf1b15cc8 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:04:32 -0700 Subject: [PATCH 025/232] stubbing out states --- .../java/org/myrobotlab/service/InMoov2.java | 997 ++++++++++-------- 1 file changed, 563 insertions(+), 434 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 908806f2a1..3fa91f12af 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -27,9 +27,11 @@ import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; +import org.myrobotlab.service.Log.LogEntry; import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; +import org.myrobotlab.service.data.Event; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -55,16 +57,6 @@ public class InMoov2 extends Service implements ServiceLifeCycleL protected static final Set stateDefaults = new TreeSet<>(); - /** - * Setting the beginning time where events are initially set. If the time - * interval has been reached: {@code - * System.currentTimeMillis() > lastFireRandomEvent - stateRandomEventTime - * } then a random event will be fired. If the fsm is in a state that can move - * on that event .. it will go to "random" state - * - */ - protected long stateLastRandomTime = System.currentTimeMillis(); - static String speechRecognizer = "WebkitSpeechRecognition"; /** @@ -118,6 +110,11 @@ public static void main(String[] args) { } } + /** + * the config that was processed before booting, if there was one. + */ + String bootedConfig = null; + protected transient ProgramAB chatBot; protected List configList; @@ -132,21 +129,14 @@ public static void main(String[] args) { */ protected boolean configStarted = false; - /** - * the config that was processed before booting, if there was one. - */ - String bootedConfig = null; - - protected transient SpeechRecognizer ear; - - protected Map ledDisplayMap = new TreeMap<>(); - /** * map of events or states to sounds */ protected Map customSoundMap = new TreeMap<>(); - protected List errors = new ArrayList<>(); + protected transient SpeechRecognizer ear; + + protected List errors = new ArrayList<>(); /** * The finite state machine is core to managing state of InMoov2. There is @@ -161,6 +151,13 @@ public static void main(String[] args) { protected Set gestures = new TreeSet(); + /** + * Prevents actions or events from happening when InMoov2 is first booted + */ + private boolean hasBooted = false; + + protected boolean isPirOn = false; + protected transient HtmlFilter htmlFilter; protected transient ImageDisplay imageDisplay; @@ -169,29 +166,30 @@ public static void main(String[] args) { protected Long lastPirActivityTime; + protected Map ledDisplayMap = new TreeMap<>(); + /** * supported locales */ protected Map locales = null; - protected long stateLastIdleTime = System.currentTimeMillis(); - protected transient SpeechSynthesis mouth; protected boolean mute = false; protected transient OpenCV opencv; + protected List peersStarted = new ArrayList<>(); + protected transient Python python; - protected String voiceSelected; + protected long stateLastIdleTime = System.currentTimeMillis(); - /** - * Prevents actions or events from happening when InMoov2 is first booted - */ - private boolean hasBooted = false; + protected long stateLastRandomTime = System.currentTimeMillis(); - protected List peersStarted = new ArrayList<>(); + protected String voiceSelected; + + protected boolean wasMutedBeforeBoot = false; public InMoov2(String n, String id) { super(n, id); @@ -216,8 +214,7 @@ public InMoov2(String n, String id) { ledDisplayMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); ledDisplayMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); - ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); - ledDisplayMap.put("pirOn", new LedDisplayData(80, 200, 90, 3, 100, 30)); + ledDisplayMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); @@ -231,18 +228,6 @@ public InMoov2(String n, String id) { } - /** - * pir active ear listening for wakeword - */ - public void idle() { - log.info("idle"); - } - - public void shutdown() { - log.info("shutdown"); - Runtime.shutdown(); - } - public void addTextListener(TextListener service) { // CORRECT WAY ! - no direct reference - just use the name in a subscription addListener("publishText", service.getName()); @@ -520,6 +505,27 @@ public void finishedGesture(String nameOfGesture) { } } + public void firstInit() { + log.info("firstInit"); + // cheap way to prevent race condition + // of "wake" firing a state change .. which will spawn + // a system event of FIRST_INIT that will answer this + // question ... + sleep(2000); + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + if (chatBot != null) { + chatBot.getResponse("FIRST_INIT"); + } + } + + public void flash(String name) { + LedDisplayData led = ledDisplayMap.get(name); + if (led == null) { + led = ledDisplayMap.get("default"); + } + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + /** * used to configure a flashing event - could use configuration to signal * different colors and states @@ -527,22 +533,17 @@ public void finishedGesture(String nameOfGesture) { * @return */ public void flash() { - // FIXME - this should be checking a protected "state" - if (!configStarted) { - if (ledDisplayMap.get("default") != null) { - LedDisplayData led = ledDisplayMap.get("default"); - invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); - } + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); } } public void flash(int r, int g, int b, int count) { // FIXME - this should be checking a protected "state" - if (!configStarted) { - if (ledDisplayMap.get("default") != null) { - LedDisplayData led = ledDisplayMap.get("default"); - invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); - } + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); } } @@ -664,6 +665,10 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } + public String getState() { + return fsm.getCurrent(); + } + /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -703,6 +708,13 @@ public void halfSpeed() { sendToPeer("torso", "setSpeed", 20.0, 20.0, 20.0); } + /** + * pir active ear listening for wakeword + */ + public void idle() { + log.info("idle"); + } + public boolean isCameraOn() { if (opencv != null) { if (opencv.isCapturing()) { @@ -936,6 +948,115 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "moveToBlocking", topStom, midStom, lowStom); } + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + synchronized public void onBoot() { + + // thinking you shouldn't "boot" twice ? + if (hasBooted) { + log.warn("will not boot again"); + return; + } + + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) + try { + // copy config if it doesn't already exist + String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + List files = FileIO.getFileList(resourceBotDir); + for (File f : files) { + String botDir = "data/config/" + f.getName(); + File bDir = new File(botDir); + if (bDir.exists() || !f.isDirectory()) { + log.info("skipping data/config/{}", botDir); + } else { + log.info("will copy new data/config/{}", botDir); + try { + FileIO.copy(f.getAbsolutePath(), botDir); + } catch (Exception e) { + error(e); + } + } + } + } catch (Exception e) { + error(e); + } + + // FIXME - find good way of running an animation "through" a state + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + invoke("publishPlayAnimation", config.bootAnimation); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG STARTED %s", bootedConfig); + } + + for (String peerKey : peersStarted) { + systemEvent("STARTED %s", peerKey); + } + + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + + fsm.fire("wake"); + + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + + hasBooted = true; + } + public PredicateEvent onChangePredicate(PredicateEvent event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { @@ -969,12 +1090,6 @@ public void onCreated(String fullname) { log.info("{} created", fullname); } - public void onError(Status status) { - if (errors.size() < 100) { - errors.add(status); - } - } - public void onGestureStatus(Status status) { if (!status.equals(Status.success()) && !status.equals(Status.warn("Python process killed !"))) { error("I cannot execute %s, please check logs", lastGestureExecuted); @@ -999,33 +1114,12 @@ public void onHeartbeat() { return; } - if (config.batteryLevelCheck) { - double batteryLevel = Runtime.getBatteryLevel(); - invoke("publishBatteryLevel", batteryLevel); - // FIXME - thresholding should always have old value or state - // so we don't pump endless errors - if (batteryLevel < 5) { - error("battery level < 5 percent"); - // systemEvent(BATTERY ERROR) - } else if (batteryLevel < 10) { - warn("battery level < 10 percent"); - // systemEvent(BATTERY WARN) - } - } - - // flash error until errors are cleared - if (config.healthCheckFlash) { - if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { - invoke("publishFlash", ledDisplayMap.get("error")); - } else if (ledDisplayMap.containsKey("heartbeat")) { - LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); - invoke("publishFlash", heartbeat); - } - } - Long lastActivityTime = getLastActivityTime(); - if (lastActivityTime != null && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + // FIXME lastActivityTime != 0 is bogus - the value should be null if + // never set + if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0 + && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { stateLastIdleTime = lastActivityTime; } @@ -1035,8 +1129,8 @@ public void onHeartbeat() { } // interval event firing - if (System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { - fsm.fire("random"); + if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + // fsm.fire("random"); stateLastRandomTime = System.currentTimeMillis(); } @@ -1044,8 +1138,48 @@ public void onHeartbeat() { error(e); } + if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) { + flash("pirOn"); + } + + if (config.batteryLevelCheck) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } + } + + // flash error until errors are cleared + if (config.healthCheckFlash) { + if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { + invoke("publishFlash", ledDisplayMap.get("error")); + } else if (ledDisplayMap.containsKey("heartbeat")) { + LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); + invoke("publishFlash", heartbeat); + } + } + + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + } + /** + * Central hub of input motion control. Potentially, all input from + * joysticks, quest2 controllers and headset, or any IK service could + * be sent here + */ @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -1065,6 +1199,22 @@ public void onJoystickInput(JoystickData input) throws Exception { // switches to manual control ? } + /** + * Centralized logging system will have all logging from all services, + * including lower level logs that do not propegate as statuses + * + * @param log + * - flushed log from Log service + */ + public void onLogEvents(List log) { + // scan for warn or errors + for (LogEntry entry : log) { + if ("ERROR".equals(entry.level) && errors.size() < 100) { + errors.add(entry); + } + } + } + public void onMoveHead(Map map) { InMoov2Head head = (InMoov2Head) getPeer("head"); if (head != null) { @@ -1086,6 +1236,10 @@ public void onMoveLeftHand(Map map) { } } + // public Message publishPython(String method, Object...data) { + // return Message.createMessage(getName(), getName(), method, data); + // } + public void onMoveRightArm(Map map) { InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); if (rightArm != null) { @@ -1107,86 +1261,19 @@ public void onMoveTorso(Map map) { } } + + +// public Message publishPython(String method, Object...data) { +// return Message.createMessage(getName(), getName(), method, data); +// } + + public OpenCVData onOpenCVData(OpenCVData data) { + // FIXME - publish event with or without data ? String file reference + return data; + } + /** - * The integration between the FiniteStateMachine (fsm) and the InMoov2 - * service and potentially other services (Python, ProgramAB) happens here. - * - * After boot all state changes get published here. - * - * Some InMoov2 service methods will be called here for "default - * implemenation" of states. If a user doesn't want to have that default - * implementation, they can change it by changing the definition of the state - * machine, and have a new state which will call a Python inmoov2 library - * callback. Overriding, appending, or completely transforming the behavior is - * all easily accomplished by managing the fsm and python inmoov2 library - * callbacks. - * - * Python inmoov2 callbacks ProgramAB topic switching - * - * Depending on config: - * - * - * @param stateChange - * @return - */ - public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { - try { - log.error("onStateChange {}", stateChange); - - String state = stateChange.current; - - // getPeer("py4j") ? - // Py4j py4j -// String code = getName()".onStateChange(" -// invoke("publishPython", "onStateChange", stateChange ); - - if (config.systemEventStateChange) { - systemEvent("ON STATE %s", state); - } - - if (config.customSounds && customSoundMap.containsKey(state)) { - invoke("publishPlayAudioFile", customSoundMap.get(state)); - } - - // TODO - only a few InMoov2 state defaults will be called here - if (stateDefaults.contains(state)) { - invoke(state); - } - - // FIXME add topic changes to AIML here ! - // FIXME add clallbacks to inmmoov2 library - - // put configurable filter here ! - - // state substitutions ? - // let python subscribe directly to fsm.publishStateChange - - // if python && configured to do python inmoov2 library callbacks - // do a callback ... default NOOPs should be in library - - // if - // invoke(state); - // depending on configuration .... - // call python ? - // fire fsm events ? - // do defaults ? - } catch (Exception e) { - error(e); - } - return stateChange; - } - -// public Message publishPython(String method, Object...data) { -// return Message.createMessage(getName(), getName(), method, data); -// } - - public OpenCVData onOpenCVData(OpenCVData data) { - // FIXME - publish event with or without data ? String file reference - return data; - } - - /** - * onPeak volume callback TODO - maybe make it variable with volume ? + * onPeak volume callback TODO - maybe make it variable with volume ? * * @param volume */ @@ -1202,40 +1289,19 @@ public void onPeak(double volume) { } /** - * initial callback for Pir sensor Default behavior will be: send fsm event - * onPirOn flash neopixel + * Pir on callback */ public void onPirOn() { - // FIXME - this should be checking a protected "state" - if (!configStarted) { - if (ledDisplayMap.get("pirOn") != null) { - LedDisplayData pirOn = ledDisplayMap.get("pirOn"); - invoke("publishFlash", pirOn); - } - } - + isPirOn = true; fsm.fire("wake"); } - public void powerDown() { - // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, - // maxInactivityTimeSeconds, maxInactivityTimeSeconds, - // maxInactivityTimeSeconds, maxInactivityTimeSeconds) - - rest(); - purgeTasks(); // including heartbeat - disable(); - - if (chatBot != null) { - chatBot.sleep(); - } - - if (ear != null) { - // FIXME - bad remove it - what is needed ? - // i think this is legacy wake word - ear.lockOutAllGrammarExcept("power up"); - } - + /** + * Pir off callback + */ + public void onPirOff() { + isPirOn = false; + fsm.fire("sleep"); } @Override @@ -1292,6 +1358,81 @@ public void onStarted(String name) { } } + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param stateChange + * @return + */ + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { + try { + log.error("onStateChange {}", stateChange); + + String current = stateChange.current; + String last = stateChange.last; + + // leaving random state + if ("random".equals(last) && !"random".equals(current) && isPeerStarted("random")) { + Random random = (Random) getPeer("random"); + random.disable(); + } + + if ("wake".equals(last)) { + invoke("publishStopAnimation"); + } + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", current); + } + + if (config.customSounds && customSoundMap.containsKey(current)) { + invoke("publishPlayAudioFile", customSoundMap.get(current)); + } + + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(current)) { + invoke(current); + } + + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library + + // put configurable filter here ! + + // state substitutions ? + // let python subscribe directly to fsm.publishStateChange + + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library + + // if + // invoke(state); + // depending on configuration .... + // call python ? + // fire fsm events ? + // do defaults ? + } catch (Exception e) { + error(e); + } + return stateChange; + } + @Override public void onStopped(String name) { log.info("service {} has stopped"); @@ -1309,6 +1450,27 @@ public void onText(String text) { invoke("publishText", text); } + public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) + + rest(); + purgeTasks(); // including heartbeat + disable(); + + if (chatBot != null) { + chatBot.sleep(); + } + + if (ear != null) { + // FIXME - bad remove it - what is needed ? + // i think this is legacy wake word + ear.lockOutAllGrammarExcept("power up"); + } + + } + /** * easy utility to publishMessage * @@ -1325,6 +1487,10 @@ public double publishBatteryLevel(double d) { return d; } + public void publishBoot() { + log.info("publishBoot"); + } + public String publishConfigFinished(String configName) { return configName; } @@ -1339,29 +1505,6 @@ public List publishConfigList() { return configList; } - public String systemEvent(String eventMsg) { - invoke("publishSystemEvent", eventMsg); - return eventMsg; - } - - public String systemEvent(String format, Object... ags) { - String eventMsg = String.format(format, ags); - return systemEvent(eventMsg); - } - - /** - * event publisher for the fsm - although other services potentially can - * consume and filter this event channel - * - * @param event - * @return - */ - public String publishSystemEvent(String event) { - // well, it turned out underscore was a goofy selection, as underscore in - // aiml is wildcard ... duh - return String.format("SYSTEM_EVENT %s", event); - } - public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, long timeOff) { LedDisplayData data = new LedDisplayData(); data.red = r; @@ -1373,6 +1516,7 @@ public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, return data; } + public LedDisplayData publishFlash(LedDisplayData data) { return data; } @@ -1384,6 +1528,15 @@ public void publishHeartbeat() { log.debug("publishHeartbeat"); } + /** + * if inactivityTime configured, this event is published after there has not + * been in activity since. + */ + public void publishInactivity() { + log.info("publishInactivity"); + fsm.fire("inactvity"); + } + /** * A more extensible interface point than publishEvent FIXME - create * interface for this @@ -1419,33 +1572,36 @@ public Map publishMoveTorso(Map map) { return map; } - public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { - log.info("publishStateChange {}", state); - return state; - } - public String publishPlayAudioFile(String filename) { return filename; } + public String publishPlayAnimation(String animation) { + return animation; + } + /** - * if inactivityTime configured, this event is published after there has not - * been in activity since. + * stop animation event */ - public void publishInactivity() { - log.info("publishInactivity"); - fsm.fire("inactvity"); + public void publishStopAnimation() { } - public void onInactivity() { - log.info("onInactivity"); - - // powerDown ? - + public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { + log.info("publishStateChange {}", state); + return state; } - public String getState() { - return fsm.getCurrent(); + /** + * event publisher for the fsm - although other services potentially can + * consume and filter this event channel + * + * @param event + * @return + */ + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh + return String.format("SYSTEM_EVENT %s", event); } /** @@ -1456,6 +1612,16 @@ public String publishText(String text) { return text; } + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + @Override public void releasePeer(String peerKey) { super.releasePeer(peerKey); @@ -1663,6 +1829,11 @@ public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist); } + // ----------------------------------------------------------------------------- + // These are methods added that were in InMoov1 that we no longer had in + // InMoov2. + // From original InMoov1 so we don't loose the + public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); } @@ -1698,16 +1869,28 @@ public void setVoice(String name) { } } - public void sleeping() { - log.error("sleeping"); - } - - public void speak(String toSpeak) { - sendToPeer("mouth", "speak", toSpeak); + public void shutdown() { + log.info("shutdown"); + Runtime.shutdown(); } - public void speakAlert(String toSpeak) { - speakBlocking(get("ALERT")); + /** + * ear still listening pir still active + */ + public void sleep() { + log.info("sleep"); + } + + public void sleeping() { + log.error("sleeping"); + } + + public void speak(String toSpeak) { + sendToPeer("mouth", "speak", toSpeak); + } + + public void speakAlert(String toSpeak) { + speakBlocking(get("ALERT")); speakBlocking(toSpeak); } @@ -1933,140 +2116,172 @@ public void startService() { } } - public void publishBoot() { - log.info("publishBoot"); + public void startServos() { + startPeer("head"); + startPeer("leftArm"); + startPeer("leftHand"); + startPeer("rightArm"); + startPeer("rightHand"); + startPeer("torso"); } - boolean wasMutedBeforeBoot = false; + // FIXME .. externalize in a json file included in InMoov2 + public Simulator startSimulator() throws Exception { + Simulator si = (Simulator) startPeer("simulator"); + return si; + } - /** - * At boot all services specified through configuration have started, or if no - * configuration has started minimally the InMoov2 service has started. During - * the processing of config and starting other services data will have - * accumulated, and at boot, some of data may now be inspected and processed - * in a synchronous single threaded way. With reporting after startup, vs - * during, other peer services are not needed (e.g. audioPlayer is no longer - * needed to be started "before" InMoov2 because when boot is called - * everything that is wanted has been started. - * - */ - synchronized public void onBoot() { + public void stop() { + sendToPeer("head", "stop"); + sendToPeer("rightHand", "stop"); + sendToPeer("leftHand", "stop"); + sendToPeer("rightArm", "stop"); + sendToPeer("leftArm", "stop"); + sendToPeer("torso", "stop"); + } - // thinking you shouldn't "boot" twice ? - if (hasBooted) { - log.warn("will not boot again"); - return; - } + public void stopGesture() { + Python p = (Python) Runtime.getService("python"); + p.stop(); + } - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); - } - } + public void stopHeartbeat() { + purgeTask("publishHeartbeat"); + config.heartbeat = false; + } - // FIXME - standardize multi-config examples should be available - // moved from startService to allow more simple control - // FIXME standard FileIO copyIfNotExists(src, dst) - try { - // copy config if it doesn't already exist - String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); - List files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/config/" + f.getName(); - File bDir = new File(botDir); - if (bDir.exists() || !f.isDirectory()) { - log.info("skipping data/config/{}", botDir); - } else { - log.info("will copy new data/config/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } - } - } catch (Exception e) { - error(e); - } + public void stopNeopixelAnimation() { + sendToPeer("neopixel", "clear"); + } - // FIXME - find good way of running an animation "through" a state - if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { - NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); - if (neoPixel != null) { - neoPixel.clear(); - neoPixel.setColor(0, 130, 0); - neoPixel.playAnimation("Larson Scanner"); + public void systemCheck() { + log.error("systemCheck()"); + Runtime runtime = Runtime.getInstance(); + int servoCount = 0; + int servoAttachedCount = 0; + for (ServiceInterface si : Runtime.getServices()) { + if (si.getClass().getSimpleName().equals("Servo")) { + servoCount++; + if (((Servo) si).getController() != null) { + servoAttachedCount++; + } } } - if (config.startupSound && getPeer("audioPlayer") != null) { - ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); - } + setPredicate("systemServoCount", servoCount); + setPredicate("systemAttachedServoCount", servoAttachedCount); + setPredicate("systemFreeMemory", Runtime.getFreeMemory()); + Platform platform = Runtime.getPlatform(); + setPredicate("system version", platform.getVersion()); + // ERROR buffer !!! + systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? + } - if (config.systemEventsOnBoot) { - // reporting on all services and config started - if (bootedConfig != null) { - // configuration was processed before booting - systemEvent("CONFIG STARTED %s", bootedConfig); - } + public String systemEvent(String eventMsg) { + invoke("publishSystemEvent", eventMsg); + return eventMsg; + } - for (String peerKey : peersStarted) { - systemEvent("STARTED %s", peerKey); - } + public String systemEvent(String format, Object... ags) { + String eventMsg = String.format(format, ags); + return systemEvent(eventMsg); + } - if (bootedConfig != null) { - // configuration was processed before booting - systemEvent("CONFIG LOADED %s", bootedConfig); - } - } + // FIXME - if this is really desired it will drive local references for all + // servos + public void waitTargetPos() { + // FIXME - consider actual reference for this + sendToPeer("head", "waitTargetPos"); + sendToPeer("rightHand", "waitTargetPos"); + sendToPeer("leftHand", "waitTargetPos"); + sendToPeer("rightArm", "waitTargetPos"); + sendToPeer("leftArm", "waitTargetPos"); + sendToPeer("torso", "waitTargetPos"); + } - // FIXME - important to do invoke & fsm needs to be consistent order + public void closeRightHand() { - // if speaking then turn off animation + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - // publish all the errors + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveRightHand", map); - // switch off animations + } - // start heartbeat - // say starting heartbeat - if (config.heartbeat) { - startHeartbeat(); - } else { - stopHeartbeat(); - } + public void openRightHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - // say finished booting + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveRightHand", map); + } + + public void closeLeftHand() { - fsm.fire("wake"); + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - // if (getPeer("mouth") != null) { - // AbstractSpeechSynthesis mouth = - // (AbstractSpeechSynthesis)getPeer("mouth"); - // mouth.setMute(wasMute); - // } + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveLeftHand", map); - hasBooted = true; } - /** - * default this will come from idle after some configurable time period - */ - public void random() { - Random random = (Random) getPeer("random"); - if (random != null) { - random.enable(); - } + public void openLeftHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveLeftHand", map); } - /** - * ear still listening pir still active - */ - public void sleep() { - log.info("sleep"); + public void openHands() { + openLeftHand(); + openRightHand(); } + public void closeHands() { + closeLeftHand(); + closeRightHand(); + } + + public Event onEvent(Event event) { + + return event; + } + public void wake() { log.info("wake"); // do waking things - based on config @@ -2137,90 +2352,4 @@ public void wake() { } } - public void firstInit() { - log.info("firstInit"); - // cheap way to prevent race condition - // of "wake" firing a state change .. which will spawn - // a system event of FIRST_INIT that will answer this - // question ... - sleep(2000); - ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); - if (chatBot != null) { - chatBot.getResponse("FIRST_INIT"); - } - } - - public void startServos() { - startPeer("head"); - startPeer("leftArm"); - startPeer("leftHand"); - startPeer("rightArm"); - startPeer("rightHand"); - startPeer("torso"); - } - - // FIXME .. externalize in a json file included in InMoov2 - public Simulator startSimulator() throws Exception { - Simulator si = (Simulator) startPeer("simulator"); - return si; - } - - public void stop() { - sendToPeer("head", "stop"); - sendToPeer("rightHand", "stop"); - sendToPeer("leftHand", "stop"); - sendToPeer("rightArm", "stop"); - sendToPeer("leftArm", "stop"); - sendToPeer("torso", "stop"); - } - - public void stopGesture() { - Python p = (Python) Runtime.getService("python"); - p.stop(); - } - - public void stopHeartbeat() { - purgeTask("publishHeartbeat"); - config.heartbeat = false; - } - - public void stopNeopixelAnimation() { - sendToPeer("neopixel", "clear"); - } - - public void systemCheck() { - log.error("systemCheck()"); - Runtime runtime = Runtime.getInstance(); - int servoCount = 0; - int servoAttachedCount = 0; - for (ServiceInterface si : Runtime.getServices()) { - if (si.getClass().getSimpleName().equals("Servo")) { - servoCount++; - if (((Servo) si).getController() != null) { - servoAttachedCount++; - } - } - } - - setPredicate("systemServoCount", servoCount); - setPredicate("systemAttachedServoCount", servoAttachedCount); - setPredicate("systemFreeMemory", Runtime.getFreeMemory()); - Platform platform = Runtime.getPlatform(); - setPredicate("system version", platform.getVersion()); - // ERROR buffer !!! - systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? - } - - // FIXME - if this is really desired it will drive local references for all - // servos - public void waitTargetPos() { - // FIXME - consider actual reference for this - sendToPeer("head", "waitTargetPos"); - sendToPeer("rightHand", "waitTargetPos"); - sendToPeer("leftHand", "waitTargetPos"); - sendToPeer("rightArm", "waitTargetPos"); - sendToPeer("leftArm", "waitTargetPos"); - sendToPeer("torso", "waitTargetPos"); - } - } From d6e43ebc6ee94c22a73cf9392ba8d043f8aa0dba Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:10:04 -0700 Subject: [PATCH 026/232] small updates --- .../service/FiniteStateMachine.java | 4 + src/main/java/org/myrobotlab/service/Git.java | 203 ++++++++++-------- .../service/InverseKinematics3D.java | 9 +- .../org/myrobotlab/service/JMonkeyEngine.java | 4 +- src/main/java/org/myrobotlab/service/Log.java | 12 +- .../java/org/myrobotlab/service/NeoPixel.java | 16 ++ .../java/org/myrobotlab/service/OakD.java | 40 +++- src/main/java/org/myrobotlab/service/Pir.java | 68 +++--- .../java/org/myrobotlab/service/Random.java | 27 ++- 9 files changed, 240 insertions(+), 143 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index 280b31b9db..221fcfcffa 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -73,6 +73,10 @@ public StateChange(String last, String current, String event) { this.current = current; this.event = event; } + + public String toString() { + return String.format("%s --%s--> %s", last, event, current); + } } private static Transition toFsmTransition(StateTransition state) { diff --git a/src/main/java/org/myrobotlab/service/Git.java b/src/main/java/org/myrobotlab/service/Git.java index 1d2d3766f5..8832326052 100644 --- a/src/main/java/org/myrobotlab/service/Git.java +++ b/src/main/java/org/myrobotlab/service/Git.java @@ -3,7 +3,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Properties; @@ -31,6 +30,7 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.submodule.SubmoduleWalk; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; @@ -45,7 +45,7 @@ public class Git extends Service { public final static Logger log = LoggerFactory.getLogger(Git.class); - transient static TextProgressMonitor monitor = new TextProgressMonitor(); + transient ProgressMonitor monitor = new ProgressMonitor(); Map repos = new TreeMap<>(); @@ -56,76 +56,43 @@ public static class RepoData { String branch; String location; String url; - List branches; String checkout; transient org.eclipse.jgit.api.Git git; - public RepoData(String location, String url, List branches, String checkout, org.eclipse.jgit.api.Git git) { + public RepoData(String location, String url, String checkout, org.eclipse.jgit.api.Git git) { this.location = location; this.url = url; - this.branches = branches; this.checkout = checkout; this.git = git; } } + // TODO - overload updates to publish + public class ProgressMonitor extends TextProgressMonitor { + + } + public Git(String n, String id) { super(n, id); } - - // max complexity clone - public void clone(String location, String url, List inbranches, String incheckout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - - File repoLocation = new File(location); - org.eclipse.jgit.api.Git git = null; - Repository repo = null; - - List branches = new ArrayList<>(); - for (String b : inbranches) { - if (!b.contains("refs")) { - branches.add("refs/heads/" + b); - } - } - - String checkout = (incheckout.contains("refs")) ? incheckout : "refs/heads/" + incheckout; - - if (!repoLocation.exists()) { - // clone - log.info("cloning {} {} into {}", url, incheckout, location); - git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranchesToClone(branches).setBranch(checkout).call(); - - } else { - // Open an existing repository - String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; - log.info("opening repo {} from {}", gitDir, url); - repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); - git = new org.eclipse.jgit.api.Git(repo); - } - - repo = git.getRepository(); - - // checkout - log.info("checking out {}", incheckout); - // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" - // + incheckout).call(); - git.branchCreate().setForce(true).setName(incheckout).setStartPoint(incheckout).call(); - git.checkout().setName(incheckout).call(); - - repos.put(location, new RepoData(location, url, inbranches, incheckout, git)); - + + public void clone(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + clone(location, url, branch, checkout, false); } + + // max complexity sync - public void sync(String location, String url, List branches, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + public void sync(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { // initial clone - clone(location, url, branches, checkout); + clone(location, url, branch, checkout); addTask(checkStatusIntervalMs, "checkStatus"); } public void sync(String location, String url, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - sync(location, url, Arrays.asList(checkout), checkout); + sync(location, url, checkout, checkout); } public RevCommit checkStatus() throws WrongRepositoryStateException, InvalidConfigurationException, InvalidRemoteException, CanceledException, RefNotFoundException, @@ -167,7 +134,7 @@ public RevCommit publishPull(RevCommit commit) { return commit; } - private static List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { List ret = new ArrayList<>(); Repository repository = git.getRepository(); @@ -193,17 +160,17 @@ public void stopSync() { purgeTasks(); } - static public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, - RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { + public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, + NoHeadException, TransportException, IOException, GitAPIException { return pull(null, null); } - static public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, + public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { return pull(null, branch); } - static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { + org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -224,7 +191,7 @@ static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { return git; } - static public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { if (src == null) { @@ -278,11 +245,11 @@ static public int pull(String src, String branch) throws IOException, WrongRepos return 0; } - static public void init() throws IllegalStateException, GitAPIException { + public void init() throws IllegalStateException, GitAPIException { init(null); } - static public void init(String directory) throws IllegalStateException, GitAPIException { + public void init(String directory) throws IllegalStateException, GitAPIException { if (directory == null) { directory = System.getProperty("user.dir"); } @@ -291,55 +258,30 @@ static public void init(String directory) throws IllegalStateException, GitAPIEx org.eclipse.jgit.api.Git git = org.eclipse.jgit.api.Git.init().setDirectory(dir).call(); } - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.INFO); - - Properties properties = Platform.gitProperties(); - Git.removeProps(); - log.info("{}", properties); - - /* - * // start the service Git git = (Git) Runtime.start("git", "Git"); - * - * // check out and sync every minute // git.sync("test", - * "https://github.com/MyRobotLab/WorkE.git", "master"); // - * git.sync("/lhome/grperry/github/mrl/myrobotlab", // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - * git.gitPull("agent-removal"); // - * git.sync(System.getProperty("user.dir"), // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - */ - } catch (Exception e) { - log.error("main threw", e); - } - } - - public static String getBranch() throws IOException { + public String getBranch() throws IOException { return getBranch(null); } - public static String getBranch(String src) throws IOException { + public String getBranch(String src) throws IOException { org.eclipse.jgit.api.Git git = getGit(src); return git.getRepository().getBranch(); } - public static Status status() throws NoWorkTreeException, IOException, GitAPIException { + public Status status() throws NoWorkTreeException, IOException, GitAPIException { return status(null); } - public static Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { + public Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { org.eclipse.jgit.api.Git git = getGit(src); Status status = git.status().call(); return status; } - public static void removeProps() { + public void removeProps() { removeProps(null); } - public static void removeProps(String rootFolder) { + public void removeProps(String rootFolder) { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -349,4 +291,89 @@ public static void removeProps(String rootFolder) { } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + Properties properties = Platform.gitProperties(); + // Git.removeProps(); + log.info("{}", properties); + Git git = (Git) Runtime.start("git", "Git"); + git.clone("./depthai", "https://github.com/luxonis/depthai.git", "main", "refs/tags/v1.13.1-sdk", true); + log.info("here"); + + + } catch (Exception e) { + log.error("main threw", e); + } + } + + // max complexity clone and checkout + public void clone(String location, String url, String branch, String checkout, boolean recursive) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + + File repoLocation = new File(location); + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // git clone + + if (!repoLocation.exists()) { + // clone + log.info("cloning {} {} checking out {} into {}", url, branch, checkout, location); + git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranch(branch).call(); + + } else { + // Open an existing repository + String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; + log.info("opening repo {} from {}", gitDir, url); + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + } + + repo = git.getRepository(); + + // git pull + + PullCommand pullCmd = git.pull() + // .setRemote(remoteName) + // .setCredentialsProvider(new + // UsernamePasswordCredentialsProvider(username, password)) + .setRemoteBranchName(branch); + + // Perform the pull operation + pullCmd.call(); + + + // recursive + if (recursive) { + + // Recursively fetch and checkout submodules if they exist + SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repo); + while (submoduleWalk.next()) { + String submodulePath = submoduleWalk.getPath(); + org.eclipse.jgit.api.Git submoduleGit = org.eclipse.jgit.api.Git.open(new File(location, submodulePath)); + submoduleGit.fetch() + .setRemote("origin") + .call(); + submoduleGit.checkout() + .setName(branch) // Replace with the desired branch name + .call(); + } + + } + + if (checkout != null) { + // checkout + log.info("checking out {}", checkout); + // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" + // + incheckout).call(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(checkout).call(); + git.checkout().setName(checkout).call(); + } + + repos.put(location, new RepoData(location, url, checkout, git)); + + } + } diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 05524a2630..378fd2440c 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -247,6 +247,12 @@ public void publishTelemetry(String name) { log.info("Servo : {} Angle : {}", jointName, angleMap.get(jointName)); } invoke("publishJointAngles", angleMap); + + InMoov2 i01 = (InMoov2)Runtime.getService("i01"); + if (i01 != null) { + i01.onJointAngles(angleMap); + } + // we want to publish the joint positions // this way we can render on the web gui.. double[][] jointPositionMap = createJointPositionMap(name); @@ -292,8 +298,7 @@ public static void main(String[] args) throws Exception { LoggingFactory.init("info"); String arm = "myArm"; - Runtime.createAndStart("python", "Python"); - Runtime.createAndStart("gui", "SwingGui"); + Runtime.start("python", "Python"); InverseKinematics3D inversekinematics = (InverseKinematics3D) Runtime.start("ik3d", "InverseKinematics3D"); // InverseKinematics3D inversekinematics = new InverseKinematics3D("iksvc"); diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index fbc5ef5e99..2174ac739b 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -1469,7 +1469,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": @@ -2157,7 +2157,7 @@ public void simpleInitApp() { new File(getDataDir()).mkdirs(); new File(getResourceDir()).mkdirs(); - // assetManager.registerLocator("./", FileLocator.class); + assetManager.registerLocator("./", FileLocator.class); assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLocator(assetsDir, FileLocator.class); assetManager.registerLocator(modelsDir, FileLocator.class); diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java index fa88732014..3e1fe48847 100644 --- a/src/main/java/org/myrobotlab/service/Log.java +++ b/src/main/java/org/myrobotlab/service/Log.java @@ -34,7 +34,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.config.LogConfig; import org.slf4j.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -44,7 +44,7 @@ import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.status.Status; -public class Log extends Service implements Appender { +public class Log extends Service implements Appender { public static class LogEntry { public long ts; @@ -81,7 +81,7 @@ public String toString() { * broadcast logging is through publishLogEvent (not broadcastState) */ transient List buffer = new ArrayList<>(); - + /** * logging state */ @@ -192,6 +192,7 @@ public void doAppend(ILoggingEvent event) throws LogbackException { synchronized public void flush() { if (buffer.size() > 0) { invoke("publishLogEvents", buffer); + buffer = new ArrayList<>(maxSize); lastPublishLogTimeTs = System.currentTimeMillis(); } @@ -224,6 +225,11 @@ public List publishLogEvents(List entries) { return entries; } + public List publishErrors(List entries) { + return entries; + } + + @Override public void setContext(Context arg0) { // TODO Auto-generated method stub diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index d9fa5e51cb..20db992f20 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -404,6 +404,14 @@ public void flash(int r, int g, int b) { public void flash(int r, int g, int b, int count) { flash(r, g, b, count, flashTimeOn, flashTimeOff); } + + public void onPlayAnimation(String animation) { + playAnimation(animation); + } + + public void onStopAnimation() { + stopAnimation(); + } public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { LedDisplayData data = new LedDisplayData(); @@ -567,6 +575,14 @@ public void playAnimation(String animation) { log.info("already playing {}", currentAnimation); return; } + +// if ("Snake".equals(animation)){ +// LedDisplayData snake = new LedDisplayData(); +// snake.red = red; +// snake.green = green; +// snake.blue = blue; +// displayQueue.add(null); +// } else if (animations.containsKey(animation)) { currentAnimation = animation; 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/Pir.java b/src/main/java/org/myrobotlab/service/Pir.java index 5a2f0d750c..eea710f585 100644 --- a/src/main/java/org/myrobotlab/service/Pir.java +++ b/src/main/java/org/myrobotlab/service/Pir.java @@ -9,7 +9,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.PirConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.PinArrayControl; import org.myrobotlab.service.interfaces.PinDefinition; @@ -52,33 +51,31 @@ public void attach(String name) { } public void setPinArrayControl(String control) { - PirConfig c = (PirConfig) config; - c.controller = control; + config.controller = control; } public void attachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { error("controller cannot be null"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin should be set before attaching"); return; } - c.controller = CodecUtils.getShortName(control); + config.controller = CodecUtils.getShortName(control); // fire and forget - send(c.controller, "attach", getName()); + send(config.controller, "attach", getName()); // assume worky isAttached = true; // enable if configured - if (c.enable) { - send(c.controller, "enablePin", c.pin, c.rate); + if (config.enable) { + send(config.controller, "enablePin", config.pin, config.rate); } broadcastState(); @@ -95,16 +92,15 @@ public void detach(String name) { * @param control */ public void detachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { log.info("detaching null"); return; } - if (c.controller != null) { - if (!c.controller.equals(control)) { - log.warn("attempting to detach {} but this pir is attached to {}", control, c.controller); + if (config.controller != null) { + if (!config.controller.equals(control)) { + log.warn("attempting to detach {} but this pir is attached to {}", control, config.controller); return; } } @@ -112,8 +108,8 @@ public void detachPinArrayControl(String control) { // disable disable(); - send(c.controller, "detach", getName()); - // c.controller = null; left as configuration .. "last controller" + send(config.controller, "detach", getName()); + // config.controller = null; left as configuration .. "last controller" // detached isAttached = false; @@ -128,13 +124,12 @@ public void detachPinArrayControl(String control) { * */ public void disable() { - PirConfig c = (PirConfig) config; - if (c.controller != null && c.pin != null) { - send(c.controller, "disablePin", c.pin); + if (config.controller != null && config.pin != null) { + send(config.controller, "disablePin", config.pin); } - c.enable = false; + config.enable = false; active = null; broadcastState(); } @@ -143,8 +138,7 @@ public void disable() { * Enables polling at the preset poll rate. */ public void enable() { - PirConfig c = (PirConfig) config; - enable(c.rate); + enable(config.rate); } /** @@ -154,14 +148,13 @@ public void enable() { * */ public void enable(int rateHz) { - PirConfig c = (PirConfig) config; - if (c.controller == null) { + if (config.controller == null) { error("pin control not set"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin not set"); return; } @@ -171,17 +164,16 @@ public void enable(int rateHz) { return; } - c.rate = rateHz; + config.rate = rateHz; /* PinArrayControl.enablePin */ - send(c.controller, "enablePin", c.pin, rateHz); - c.enable = true; + send(config.controller, "enablePin", config.pin, rateHz); + config.enable = true; broadcastState(); } @Override public String getPin() { - PirConfig c = (PirConfig) config; - return c.pin; + return config.pin; } /** @@ -190,8 +182,7 @@ public String getPin() { * @return Hz */ public int getRate() { - PirConfig c = (PirConfig) config; - return c.rate; + return config.rate; } /** @@ -211,8 +202,7 @@ public boolean isActive() { * @return true = Enabled. false = Disabled. */ public boolean isEnabled() { - PirConfig c = (PirConfig) config; - return c.enable; + return config.enable; } @Override @@ -247,13 +237,12 @@ public PirConfig getConfig() { @Override public void onPin(PinData pindata) { - PirConfig c = (PirConfig) config; log.debug("onPin {}", pindata); boolean sense = (pindata.value != 0); // sparse publishing only on state change - if (active == null || active != sense && c.enable) { + if (active == null || active != sense && config.enable) { // state change invoke("publishSense", sense); active = sense; @@ -286,14 +275,12 @@ public void publishPirOff() { */ @Override public void setPin(String pin) { - PirConfig c = (PirConfig) config; - c.pin = pin; + config.pin = pin; } @Deprecated /* use attach(String) */ public void setPinArrayControl(PinArrayControl pinControl) { - PirConfig c = (PirConfig) config; - c.controller = pinControl.getName(); + config.controller = pinControl.getName(); } /** @@ -306,8 +293,7 @@ public void setRate(int rateHz) { error("invalid poll rate - default is 1 Hz valid value is > 0"); return; } - PirConfig c = (PirConfig) config; - c.rate = rateHz; + config.rate = rateHz; } /** diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index 93b8178fd2..2d435ac35d 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -183,7 +183,10 @@ public void addRandom(long minIntervalMs, long maxIntervalMs, String name, Strin msg.interval = getRandom(minIntervalMs, maxIntervalMs); log.info("add random message {} in {} ms", key, msg.interval); - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } broadcastState(); } @@ -230,7 +233,11 @@ public void process(String key) { purgeTask(key); if (!msg.oneShot) { msg.interval = getRandom(msg.minIntervalMs, msg.maxIntervalMs); - addTask(key, 0, msg.interval, "process", key); + // must re-schedule unless one shot + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } } } @@ -316,7 +323,10 @@ public void enable(String key) { return; } randomData.get(key).enabled = true; - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } return; } // must be name - disable "all" for this service @@ -325,7 +335,10 @@ public void enable(String key) { if (msg.name.equals(name)) { msg.enabled = true; String fullKey = String.format("%s.%s", msg.name, msg.method); - addTask(fullKey, 0, msg.interval, "process", fullKey); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(fullKey, 0, msg.interval, "process", fullKey); + } } } } @@ -335,6 +348,7 @@ public void disable() { // events purgeTasks(); enabled = false; + broadcastState(); } public void enable() { @@ -345,7 +359,9 @@ public void enable() { addTask(fullKey, 0, msg.interval, "process", fullKey); } } + enabled = true; + broadcastState(); } public void purge() { @@ -392,9 +408,10 @@ public static void main(String[] args) { List ret = random.getServiceList(); Set mi = random.getMethodsFromName("c1"); List mes = MethodCache.getInstance().query("Clock", "setInterval"); - + random.disable(); random.addRandom(200, 1000, "i01", "setHeadSpeed", 8, 20, 8, 20, 8, 20); random.addRandom(200, 1000, "i01", "moveHead", 65, 115, 65, 115, 65, 115); + random.enable(); // Python python = (Python) Runtime.start("python", "Python"); From dcae69235e339d9747b239ed4f46f2cf5a3b729c Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:12:43 -0700 Subject: [PATCH 027/232] intermediate --- .../org/myrobotlab/framework/Service.java | 2 +- .../org/myrobotlab/service/InMoov2Head.java | 37 +++++-- .../java/org/myrobotlab/service/Updater.java | 98 ++++++++++++++++++- .../service/config/InMoov2Config.java | 81 +++++++++++---- 4 files changed, 186 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 1a3d9e6956..c5f5cc22ac 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -1525,7 +1525,7 @@ public QueueStats publishQueueStats(QueueStats stats) { * @return the service */ @Override - public Service publishState() { + public Service publishState() { return this; } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index 63b34b1198..bff2bb394e 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -142,15 +142,34 @@ public void disable() { eyelidRight.disable(); } - public long getLastActivityTime() { - - long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + public Long getLastActivityTime() { + + Long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); + if (getPeer("eyeX") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); + } + if (getPeer("eyeY") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); + } + if (getPeer("jaw") != null) { + lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rothead.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, neck.getLastActivityTime()); + } + + if (getPeer("eyelidLeft") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); + } + if (getPeer("eyelidRight") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + } return lastActivityTime; } diff --git a/src/main/java/org/myrobotlab/service/Updater.java b/src/main/java/org/myrobotlab/service/Updater.java index 779dc1c81f..f97f3149e6 100644 --- a/src/main/java/org/myrobotlab/service/Updater.java +++ b/src/main/java/org/myrobotlab/service/Updater.java @@ -4,11 +4,32 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Properties; import java.util.Set; import java.util.TreeSet; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.DetachedHeadException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.BranchTrackingStatus; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.CmdOptions; import org.myrobotlab.framework.MrlException; @@ -392,10 +413,13 @@ public void checkForUpdates() { if (isSrcMode) { String cwd = System.getProperty("user.dir"); boolean makeBuild = false; - String branch = Git.getBranch(); + + Repository repo = new FileRepositoryBuilder().setGitDir(new File(System.getProperty("user.dir"))).build(); + org.eclipse.jgit.api.Git git = new org.eclipse.jgit.api.Git(repo); + String branch = git.getRepository().getBranch(); log.info("current source branch is \"{}\"", branch); - int commitsBehind = Git.pull(branch); + int commitsBehind = pull(null, branch); if (gitProps == null) { log.info("target/classes/git.properties does not exist - will build"); @@ -413,7 +437,8 @@ public void checkForUpdates() { // FIXME - download mvn if it does not exist ?? // remove git properties before compile - Git.removeProps(); + File props = new File(System.getProperty("user.dir") + File.separator + "target" + File.separator + "classes" + File.separator + "git.properties"); + props.delete(); // FIXME - compile or package mode ! String ret = Maven.mvn(cwd, branch, "compile", System.currentTimeMillis() / 1000, offline); @@ -613,6 +638,73 @@ public boolean accept(File dir, String name) { return false; } + TextProgressMonitor monitor = new TextProgressMonitor(); + + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { + + if (src == null) { + src = System.getProperty("user.dir"); + } + + if (branch == null) { + log.warn("branch is not set - setting to default develop"); + branch = "develop"; + } + + List branches = new ArrayList(); + branches.add("refs/heads/" + branch); + + File repoParentFolder = new File(src); + + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // Open an existing repository FIXME Try Git.open(dir) + String gitDir = repoParentFolder.getAbsolutePath() + "/.git"; + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + + repo = git.getRepository(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(branch).call(); + git.checkout().setName(branch).call(); + + git.fetch().setProgressMonitor(monitor).call(); + + List localLogs = getLogs(git, "origin/" + branch, 1); + List remoteLogs = getLogs(git, "remotes/origin/" + branch, 1); + + RevCommit localCommit = localLogs.get(0); + RevCommit remoteCommit = remoteLogs.get(0); + + BranchTrackingStatus status = BranchTrackingStatus.of(repo, branch); + + // FIXME - Git.close() file handles + + if (status.getBehindCount() > 0) { + log.info("local ts {}, remote {} - {} pulling", localCommit.getCommitTime(), remoteCommit.getCommitTime(), remoteCommit.getFullMessage()); + PullCommand pullCmd = git.pull(); + pullCmd.setProgressMonitor(monitor); + pullCmd.call(); + git.close(); + return status.getBehindCount(); + } + log.info("no new commits on branch {}", branch); + git.close(); + return 0; + } + + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { + List ret = new ArrayList<>(); + Repository repository = git.getRepository(); + Iterable logs = git.log().setMaxCount(maxCount).add(repository.resolve(ref)).call(); + for (RevCommit rev : logs) { + ret.add(rev); + } + return ret; + } + public static void main(String[] args) { LoggingFactory.init(Level.INFO); diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 7c188e95d5..3634fe4e47 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -92,6 +92,8 @@ public class InMoov2Config extends ServiceConfig { public boolean pirEnableTracking = false; + public boolean pirOnFlash = true; + /** * play pir sounds when pir switching states sound located in * data/InMoov2/sounds/pir-activated.mp3 sound located in @@ -100,9 +102,9 @@ public class InMoov2Config extends ServiceConfig { public boolean pirPlaySounds = true; public boolean pirWakeUp = true; - - public boolean robotCanMoveHeadWhileSpeaking = true; + public boolean robotCanMoveHeadWhileSpeaking = true; + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest @@ -115,36 +117,36 @@ public class InMoov2Config extends ServiceConfig { public int sleepTimeoutMs = 300000; public boolean startupSound = true; - - /** - * Determines if InMoov2 publish system events during boot state - */ - public boolean systemEventsOnBoot = false; - - /** - * Publish system event when state changes - */ - public boolean systemEventStateChange = true; /** * */ - public boolean stateChangeIsMute = true; - + public boolean stateChangeIsMute = true; /** * Interval in seconds for a idle state event to fire off. * If the fsm is in a state which will allow transitioning, the InMoov2 * state will transition to idle. Heartbeat will fire the event. */ - public int stateIdleInterval = 120; - + public Integer stateIdleInterval = 120; + + /** * Interval in seconds for a random state event to fire off. * If the fsm is in a state which will allow transitioning, the InMoov2 * state will transition to random. Heartbeat will fire the event. */ - public int stateRandomInterval = 120; + public Integer stateRandomInterval = 120; + + /** + * Determines if InMoov2 publish system events during boot state + */ + public boolean systemEventsOnBoot = false; + + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; public int trackingTimeoutMs = 10000; @@ -152,6 +154,8 @@ public class InMoov2Config extends ServiceConfig { public boolean virtual = false; + public String bootAnimation = "Theater Chase"; + public InMoov2Config() { } @@ -176,6 +180,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "left", "Arduino", false); addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false); addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false); + addDefaultPeerConfig(plan, name, "log", "Log", false); addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false); // a first ! addDefaultPeerConfig(plan, name, "mouth.audioFile", "AudioFile", false); @@ -338,6 +343,7 @@ public Plan getDefault(Plan plan, String name) { fsm.transitions.add(new Transition("idle", "random", "random")); fsm.transitions.add(new Transition("random", "idle", "idle")); fsm.transitions.add(new Transition("idle", "sleep", "sleep")); + fsm.transitions.add(new Transition("sleep", "wake", "wake")); fsm.transitions.add(new Transition("idle", "powerDown", "powerDown")); fsm.transitions.add(new Transition("wake", "firstInit", "firstInit")); // powerDown to shutdown @@ -345,10 +351,11 @@ public Plan getDefault(Plan plan, String name) { // fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); PirConfig pir = (PirConfig) plan.get(getPeerName("pir")); - pir.pin = "23"; + pir.pin = "D23"; pir.controller = name + ".left"; pir.listeners = new ArrayList<>(); - pir.listeners.add(new Listener("publishPirOn", name, "onPirOn")); + pir.listeners.add(new Listener("publishPirOn", name)); + pir.listeners.add(new Listener("publishPirOff", name)); // == Peer - random ============================= RandomConfig random = (RandomConfig) plan.get(getPeerName("random")); @@ -472,6 +479,42 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishConfigFinished", name)); listeners.add(new Listener("publishStateChange", name)); +// listeners.add(new Listener("publishPowerUp", name)); +// listeners.add(new Listener("publishPowerDown", name)); +// listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + // service --to--> InMoov2 + AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); + mouth_audioFile.listeners = new ArrayList<>(); + mouth_audioFile.listeners.add(new Listener("publishPeak", name)); + fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); + + + LogConfig log = (LogConfig) plan.get(getPeerName("log")); + log.listeners = new ArrayList<>(); + log.listeners.add(new Listener("publishLogEvents", name)); + +// mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); +// mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + + // InMoov2 --to--> service + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay")); + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + + listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel"))); + listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel"))); + + // remove the auto-added starts in the plan's runtime RuntimConfig.registry + plan.removeStartsWith(name + "."); + // listeners.add(new Listener("publishPowerUp", name)); // listeners.add(new Listener("publishPowerDown", name)); // listeners.add(new Listener("publishError", name)); From 332389712acb467dfdf4ba47de2db234bdf29264 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:13:10 -0700 Subject: [PATCH 028/232] caliko --- pom.xml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pom.xml b/pom.xml index 0900d2149e..45c2647161 100644 --- a/pom.xml +++ b/pom.xml @@ -1674,6 +1674,18 @@ + + au.edu.federation.caliko + caliko + 1.3.8 + + + + au.edu.federation.caliko.visualisation + caliko-visualisation + 1.3.8 + + com.github.sarxos @@ -1762,6 +1774,12 @@ + + dev.onvoid.webrtc + webrtc-java + 0.7.0 + + org.mockito @@ -1769,6 +1787,11 @@ 3.12.4 test + + au.edu.federation.caliko.demo + caliko-demo + 1.3.8 + From 1d3f08cb1009defae479cb78775c775c875affd4 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:15:55 -0700 Subject: [PATCH 029/232] updates --- src/main/java/org/myrobotlab/kinematics/DHRobotArm.java | 5 ++++- src/main/java/org/myrobotlab/service/InMoov2.java | 2 ++ src/main/java/org/myrobotlab/service/Random.java | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java index 03b8c251b9..6fd3e43b13 100644 --- a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java +++ b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java @@ -235,7 +235,10 @@ public boolean moveToGoal(Point goal) { int numSteps = 0; double iterStep = 0.05; // we're in millimeters.. - double errorThreshold = 2.0; + double errorThreshold = 20.0; + + maxIterations = 1000; + // what's the current point while (true) { numSteps++; diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 3fa91f12af..5b3744fea8 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -59,6 +59,8 @@ public class InMoov2 extends Service implements ServiceLifeCycleL static String speechRecognizer = "WebkitSpeechRecognition"; + protected static final Set stateDefaults = new TreeSet<>(); + /** * This method will load a python file into the python interpreter. * diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index 2d435ac35d..dca027aa3a 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -191,9 +191,9 @@ public void addRandom(long minIntervalMs, long maxIntervalMs, String name, Strin } public void process(String key) { - if (!enabled) { - return; - } + // if (!enabled) { + // return; + // } RandomMessage msg = randomData.get(key); if (msg == null || !msg.enabled) { From 9c875a8d68f75ff51872741118cf07902c0a3ef6 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:24:32 -0700 Subject: [PATCH 030/232] transfer --- pom.xml | 971 +++++++++--------- .../org/myrobotlab/caliko/Application.java | 82 ++ .../org/myrobotlab/caliko/CalikoDemo3D.java | 148 +++ .../org/myrobotlab/caliko/FabrikModel3D.java | 316 ++++++ .../myrobotlab/caliko/GuiDemoStructure.java | 30 + .../java/org/myrobotlab/caliko/Model.java | 928 +++++++++++++++++ .../org/myrobotlab/caliko/OpenGLWindow.java | 505 +++++++++ .../org/myrobotlab/kinematics/DHRobotArm.java | 5 +- .../java/org/myrobotlab/service/Caliko.java | 401 ++++++++ .../service/FiniteStateMachine.java | 15 + src/main/java/org/myrobotlab/service/Git.java | 203 ++-- .../java/org/myrobotlab/service/InMoov2.java | 948 +++++++++++++---- .../org/myrobotlab/service/InMoov2Head.java | 37 +- .../service/InverseKinematics3D.java | 9 +- .../org/myrobotlab/service/JMonkeyEngine.java | 4 +- src/main/java/org/myrobotlab/service/Log.java | 12 +- .../java/org/myrobotlab/service/NeoPixel.java | 50 + .../java/org/myrobotlab/service/OakD.java | 40 +- src/main/java/org/myrobotlab/service/Pir.java | 68 +- .../java/org/myrobotlab/service/Random.java | 33 +- .../java/org/myrobotlab/service/Updater.java | 98 +- .../service/config/CalikoConfig.java | 36 + .../service/config/InMoov2Config.java | 111 +- .../myrobotlab/service/config/LogConfig.java | 12 + .../myrobotlab/service/config/OakDConfig.java | 32 + .../service/config/WebXRConfig.java | 37 + .../org/myrobotlab/service/data/Event.java | 38 + .../myrobotlab/service/meta/CalikoMeta.java | 41 + .../resource/BoofCV/basket_depth.png | Bin 0 -> 94310 bytes .../resources/resource/BoofCV/basket_rgb.png | Bin 0 -> 579028 bytes .../resources/resource/BoofCV/intrinsic.yaml | 19 + .../resource/BoofCV/visualdepth.yaml | 20 + src/main/resources/resource/Caliko.png | Bin 0 -> 1525 bytes .../resources/resource/Caliko/pyramid.obj | 56 + .../WebGui/app/service/js/WebXRGui.js | 43 + .../WebGui/app/service/views/WebXRGui.html | 64 ++ src/main/resources/resource/WebXR.png | Bin 0 -> 20829 bytes .../myrobotlab/service/ServoMixerTest.java | 31 + .../myrobotlab/service/WebGuiSocketTest.java | 137 +++ 39 files changed, 4715 insertions(+), 865 deletions(-) create mode 100644 src/main/java/org/myrobotlab/caliko/Application.java create mode 100644 src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java create mode 100644 src/main/java/org/myrobotlab/caliko/FabrikModel3D.java create mode 100644 src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java create mode 100644 src/main/java/org/myrobotlab/caliko/Model.java create mode 100644 src/main/java/org/myrobotlab/caliko/OpenGLWindow.java create mode 100644 src/main/java/org/myrobotlab/service/Caliko.java create mode 100644 src/main/java/org/myrobotlab/service/config/CalikoConfig.java create mode 100644 src/main/java/org/myrobotlab/service/config/LogConfig.java create mode 100644 src/main/java/org/myrobotlab/service/config/OakDConfig.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/meta/CalikoMeta.java create mode 100644 src/main/resources/resource/BoofCV/basket_depth.png create mode 100644 src/main/resources/resource/BoofCV/basket_rgb.png create mode 100644 src/main/resources/resource/BoofCV/intrinsic.yaml create mode 100644 src/main/resources/resource/BoofCV/visualdepth.yaml create mode 100644 src/main/resources/resource/Caliko.png create mode 100644 src/main/resources/resource/Caliko/pyramid.obj 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/ServoMixerTest.java create mode 100644 src/test/java/org/myrobotlab/service/WebGuiSocketTest.java diff --git a/pom.xml b/pom.xml index a1507381a2..259b4e77b4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,102 +1,102 @@ - - - 4.0.0 - org.myrobotlab - mrl - 0.0.1-SNAPSHOT - MyRobotLab - Open Source Creative Machine Control - - - false - - - - 1.1. - - ${maven.build.timestamp} - yyyyMMddHHmm - ${timestamp} - ${version.prefix}${build.number} - ${git.branch} - ${NODE_NAME} - ${NODE_LABELS} - - - - 11 - 11 - UTF-8 - - - + + + 4.0.0 + org.myrobotlab + mrl + 0.0.1-SNAPSHOT + MyRobotLab + Open Source Creative Machine Control + + + false + + + + 1.1. + + ${maven.build.timestamp} + yyyyMMddHHmm + ${timestamp} + ${version.prefix}${build.number} + ${git.branch} + ${NODE_NAME} + ${NODE_LABELS} + + + + 11 + 11 + UTF-8 + + + @@ -135,9 +135,9 @@ https://m2.dv8tion.net/releases - - - + + + javazoom @@ -1646,6 +1646,18 @@ + + au.edu.federation.caliko + caliko + 1.3.8 + + + + au.edu.federation.caliko.visualisation + caliko-visualisation + 1.3.8 + + com.github.sarxos @@ -1734,375 +1746,386 @@ - - - org.mockito - mockito-core - 3.12.4 - test - - - - - - - false - src/main/resources - - - false - src/main/java - - ** - - - **/*.java - - - - - - false - src/test/resources - - - false - src/test/java - - ** - - - **/*.java - - - - src/main/resources - ${project.basedir} - - - - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0.0 - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.1.0 - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - no-duplicate-declared-dependencies - - enforce - - - - - - - - - - - - org.codehaus.mojo - properties-maven-plugin - - - initialize - - read-project-properties - - - - build.properties - - - - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.1.0 - - - package - - shade - - - myrobotlab - - true - myrobotlab-full - false - - - - - org.myrobotlab.service.Runtime - ${version} - ${version} - - ${build.number} - ${maven.build.timestamp} - ${agent.name} - ${user.name} - - - ${git.tags} - ${git.branch} - ${git.dirty} - ${git.remote.origin.url} - ${git.commit.id} - ${git.commit.id.abbrev} - ${git.commit.id.full} - ${git.commit.id.describe} - ${git.commit.id.describe-short} - ${git.commit.user.name} - ${git.commit.user.email} - - ${git.commit.time} - ${git.closest.tag.name} - ${git.closest.tag.commit.count} - ${git.build.user.name} - ${git.build.user.email} - ${git.build.time} - ${git.build.version} - - - - - - - *:* - - module-info.class - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assembly.xml - - myrobotlab - false - - - - trigger-assembly - package - - single - - - - - - - true - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 11 - 11 - true - true - -parameters - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - - - pl.project13.maven - git-commit-id-plugin - 4.9.10 - - - initialize - get-the-git-infos - - revision - - - - - ${project.basedir}/.git - git - false - true - ${project.build.outputDirectory}/git.properties - - - false - false - -dirty - - - - - - maven-surefire-plugin - org.apache.maven.plugins - 2.22.2 - - -Djava.library.path=libraries/native -Djna.library.path=libraries/native - - **/*Test.java - - - **/integration/* - - - - - - - - org.apache.maven.plugins - maven-clean-plugin - 2.3 - - - - data/.myrobotlab - false - - - libraries - - ** - - false - - - data - - ** - - - - resource - - ** - - - - src/main/resources/resource/framework - - **/serviceData.json - - false - - - - - - - - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.22.2 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - - - myrobotlab - http://myrobotlab.org - - - github - https://github.com/MyRobotLab/myrobotlab/issues - - + + dev.onvoid.webrtc + webrtc-java + 0.7.0 + + + + + org.mockito + mockito-core + 3.12.4 + test + + + au.edu.federation.caliko.demo + caliko-demo + 1.3.8 + + + + + + + false + src/main/resources + + + false + src/main/java + + ** + + + **/*.java + + + + + + false + src/test/resources + + + false + src/test/java + + ** + + + **/*.java + + + + src/main/resources + ${project.basedir} + + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + no-duplicate-declared-dependencies + + enforce + + + + + + + + + + + + org.codehaus.mojo + properties-maven-plugin + + + initialize + + read-project-properties + + + + build.properties + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + myrobotlab + + true + myrobotlab-full + false + + + + + org.myrobotlab.service.Runtime + ${version} + ${version} + + ${build.number} + ${maven.build.timestamp} + ${agent.name} + ${user.name} + + + ${git.tags} + ${git.branch} + ${git.dirty} + ${git.remote.origin.url} + ${git.commit.id} + ${git.commit.id.abbrev} + ${git.commit.id.full} + ${git.commit.id.describe} + ${git.commit.id.describe-short} + ${git.commit.user.name} + ${git.commit.user.email} + + ${git.commit.time} + ${git.closest.tag.name} + ${git.closest.tag.commit.count} + ${git.build.user.name} + ${git.build.user.email} + ${git.build.time} + ${git.build.version} + + + + + + + *:* + + module-info.class + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assembly.xml + + myrobotlab + false + + + + trigger-assembly + package + + single + + + + + + + true + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 11 + 11 + true + true + -parameters + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + + initialize + get-the-git-infos + + revision + + + + + ${project.basedir}/.git + git + false + true + ${project.build.outputDirectory}/git.properties + + + false + false + -dirty + + + + + + maven-surefire-plugin + org.apache.maven.plugins + 2.22.2 + + -Djava.library.path=libraries/native -Djna.library.path=libraries/native + + **/*Test.java + + + **/integration/* + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 2.3 + + + + data/.myrobotlab + false + + + libraries + + ** + + false + + + data + + ** + + + + resource + + ** + + + + src/main/resources/resource/framework + + **/serviceData.json + + false + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + + + myrobotlab + http://myrobotlab.org + + + github + https://github.com/MyRobotLab/myrobotlab/issues + + diff --git a/src/main/java/org/myrobotlab/caliko/Application.java b/src/main/java/org/myrobotlab/caliko/Application.java new file mode 100644 index 0000000000..d8474c157e --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/Application.java @@ -0,0 +1,82 @@ +package org.myrobotlab.caliko; + +import static org.lwjgl.glfw.GLFW.glfwPollEvents; +import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.glClear; + +import org.myrobotlab.service.Caliko; + +import au.edu.federation.caliko.demo.CalikoDemo; +import au.edu.federation.utils.Vec3f; + +/** + * An example application to demonstrate the Caliko library in both 2D and 3D + * modes. + * + * Use up/down cursors to change between 2D/3D mode and left/right cursors to + * change demos. In 2D mode clicking using the left mouse button (LMB) changes + * the target location, and you can click and drag. In 3D mode, use W/S/A/D to + * move the camera and the mouse with LMB held down to look. + * + * See the README.txt for further documentation and controls. + * + * @author Al Lansley + * @version 1.0 - 31/01/2016 + */ +public class Application { + // Define cardinal axes + final Vec3f X_AXIS = new Vec3f(1.0f, 0.0f, 0.0f); + final Vec3f Y_AXIS = new Vec3f(0.0f, 1.0f, 0.0f); + final Vec3f Z_AXIS = new Vec3f(0.0f, 0.0f, 1.0f); + + // State tracking variables + boolean use3dDemo = true; + int demoNumber = 7; + boolean fixedBaseMode = true; + boolean rotateBasesMode = false; + boolean drawLines = true; + boolean drawAxes = false; + boolean drawModels = true; + boolean drawConstraints = true; + boolean leftMouseButtonDown = false; + boolean paused = true; + + // Create our window and OpenGL context + int windowWidth = 800; + int windowHeight = 600; + public OpenGLWindow window = null; + + // Declare a CalikoDemo object which can run our 3D and 2D demonstration + // scenarios + transient private CalikoDemo demo; + transient private Caliko service; + public boolean running = true; + + public Application(Caliko service) { + this.service = service; + window = new OpenGLWindow(this, service, windowWidth, windowHeight); + demo = new CalikoDemo3D(this); + mainLoop(); + window.cleanup(); + } + + public Caliko getService() { + return service; + } + + private void mainLoop() { + // Run the rendering loop until the user closes the window or presses Escape + while (!glfwWindowShouldClose(window.mWindowId) && running) { + // Clear the screen and depth buffer then draw the demo + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + demo.draw(); + + // Swap the front and back colour buffers and poll for events + window.swapBuffers(); + glfwPollEvents(); + } + } + +} // End of Application class diff --git a/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java b/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java new file mode 100644 index 0000000000..8e977fe9d7 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/CalikoDemo3D.java @@ -0,0 +1,148 @@ +package org.myrobotlab.caliko; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.demo.CalikoDemo; +import au.edu.federation.caliko.demo3d.CalikoDemoStructure3D; +import au.edu.federation.caliko.visualisation.Axis; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.caliko.visualisation.FabrikConstraint3D; +import au.edu.federation.caliko.visualisation.FabrikLine3D; +// import au.edu.federation.caliko.visualisation.FabrikModel3D; +import au.edu.federation.caliko.visualisation.Grid; +import au.edu.federation.caliko.visualisation.MovingTarget3D; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec3f; + +/** + * Class to demonstrate some of the features of the Caliko library in 3D. + * + * @author Al Lansley + * @version 0.7.1 - 20/07/2016 + */ +public class CalikoDemo3D implements CalikoDemo +{ + static float defaultBoneLength = 10.0f; + static float boneLineWidth = 5.0f; + static float constraintLineWidth = 2.0f; + static float baseRotationAmountDegs = 0.3f; + + // Set yo a camera which we'll use to navigate. Params: location, orientation, width and height of window. + static Camera camera = new Camera(new Vec3f(0.0f, 00.0f, 150.0f), new Vec3f(), 800, 600); + + // Setup some grids to aid orientation + static float extent = 1000.0f; + static float gridLevel = 100.0f; + static int subdivisions = 20; + static Grid lowerGrid = new Grid(extent, extent, -gridLevel, subdivisions); + static Grid upperGrid = new Grid(extent, extent, gridLevel, subdivisions); + + // An axis to show the X/Y/Z orientation of each bone. Params: Axis length, axis line width + static Axis axis = new Axis(3.0f, 1.0f); + + // A constraint we can use to draw any joint angle restrictions of ball and hinge joints + static FabrikConstraint3D constraint = new FabrikConstraint3D(); + + // A simple Wavefront .OBJ format model of a pyramid to display around each bone (set to draw with a 1.0f line width) + static FabrikModel3D model = new FabrikModel3D("/pyramid.obj", 1.0f); + + // Setup moving target. Params: location, extents, interpolation frames, grid height for vertical bar + static MovingTarget3D target = new MovingTarget3D(new Vec3f(0, -30, 0), new Vec3f(60.0f), 200, gridLevel); + + private FabrikStructure3D mStructure; + + private CalikoDemoStructure3D demoStructure3d; + + private transient Application application; + + /** + * Constructor. + * + * @param demoNumber The number of the demo to set up. + */ + public CalikoDemo3D(Application application) + { + this.application = application; + setup(0); + } + + /** + * Set up a demo consisting of an arrangement of 3D IK chains with a given configuration. + * + * @param demoNumber The number of the demo to set up. + */ + public void setup(int demoNumber) + { + this.mStructure = application.getService().getStructure(); + + this.demoStructure3d = new GuiDemoStructure(application.getService(), null); + + // Set the appropriate window title and make an initial solve pass of the structure + application.window.setWindowTitle(this.mStructure.getName()); + //structure.updateTarget( target.getCurrentLocation() ); + } + + /** Set all chains in the structure to be in fixed-base mode whereby the base locations cannot move. */ + public void setFixedBaseMode(boolean value) { mStructure.setFixedBaseMode(value); } + + /** Handle the movement of the camera using the W/S/A/D keys. */ + public void handleCameraMovement(int key, int action) { camera.handleKeypress(key, action); } + + public void draw() + { + // Move the camera based on keypresses and mouse movement + camera.move(1.0f / 60.0f); + + // Get the ModelViewProjection matrix as we use it multiple times + Mat4f mvpMatrix = application.window.getMvpMatrix(); + + // Draw our grids + lowerGrid.draw(mvpMatrix); + upperGrid.draw(mvpMatrix); + + // If we're not paused then step the target and solve the structure for the new target location + if (!application.paused) + { + target.step(); + this.demoStructure3d.drawTarget(mvpMatrix); + + // Solve the structure (chains with embedded targets will use those, otherwise the provided target is used) + mStructure.solveForTarget( target.getCurrentLocation() ); + + FabrikChain3D chain = application.getService().getChain("default"); + + for (FabrikBone3D bone : chain.getChain()) { + bone.getStartLocation().getGlobalPitchDegs(); + bone.getStartLocation().getGlobalYawDegs(); + + System.out.println("Bone X: " + bone.getStartLocation().toString()); + } + + } + + // If we're in rotate base mode then rotate the base location(s) of all chains in the structure + if (application.rotateBasesMode) + { + int numChains = mStructure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + Vec3f base = mStructure.getChain(loop).getBaseLocation(); + base = Vec3f.rotateAboutAxisDegs(base, baseRotationAmountDegs, CalikoDemoStructure3D.Y_AXIS); + mStructure.getChain(loop).setBaseLocation(base); + } + } + + // Draw the target + target.draw(Utils.YELLOW, 8.0f, mvpMatrix); + + // Draw the structure as required + // Note: bone lines are drawn in the bone colour, models are drawn in white by default but you can specify a colour to the draw method, + // axes are drawn X/Y/Z as Red/Green/Blue and constraints are drawn the colours specified in the FabrikConstraint3D class. + if (application.drawLines) { FabrikLine3D.draw(mStructure, boneLineWidth, mvpMatrix); } + if (application.drawModels) { model.drawStructure(mStructure, camera.getViewMatrix(), application.window.mProjectionMatrix); } + if (application.drawAxes) { axis.draw(mStructure, camera.getViewMatrix(), application.window.mProjectionMatrix); } + if (application.drawConstraints) { constraint.draw(mStructure, constraintLineWidth, mvpMatrix); } + } +} diff --git a/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java b/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java new file mode 100644 index 0000000000..470f98b01f --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/FabrikModel3D.java @@ -0,0 +1,316 @@ +package org.myrobotlab.caliko; + +import java.nio.FloatBuffer; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.visualisation.ShaderProgram; +import au.edu.federation.utils.Colour4f; +import au.edu.federation.utils.Mat3f; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; + +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL15.*; +import static org.lwjgl.opengl.GL20.*; +import static org.lwjgl.opengl.GL30.*; + +/** + * A class to represent a 3D model that can easily be attached to a FabrikBone3D object. + * + * @author Al Lansley + * @version 0.3.1 - 20/07/2016 + */ +public class FabrikModel3D +{ + // Each vertex has three positional components - the x, y and z values. + private static final int VERTEX_COMPONENTS = 3; + + // A single static ShaderProgram is used to draw all axes + private static ShaderProgram shaderProgram; + + // Vertex shader source + private static final String VERTEX_SHADER_SOURCE = + "#version 330" + Utils.NEW_LINE + + "in vec3 vertexLocation; // Incoming vertex attribute" + Utils.NEW_LINE + + "uniform mat4 mvpMatrix; // Combined Model/View/Projection matrix " + Utils.NEW_LINE + + "void main(void) {" + Utils.NEW_LINE + + " gl_Position = mvpMatrix * vec4(vertexLocation, 1.0); // Project our geometry" + Utils.NEW_LINE + + "}"; + + // Fragment shader source + private static final String FRAGMENT_SHADER_SOURCE = + "#version 330" + Utils.NEW_LINE + + "out vec4 outputColour;" + Utils.NEW_LINE + + "uniform vec4 colour;" + Utils.NEW_LINE + + "void main() {" + Utils.NEW_LINE + + " outputColour = colour;" + Utils.NEW_LINE + + "}"; + + // Hold id values for the Vertex Array Object (VAO) and Vertex Buffer Object (VBO) + private static int vaoId; + private static int vboId; + + // Float buffers for the ModelViewProjection matrix and model colour + private static FloatBuffer mvpMatrixFB; + private static FloatBuffer colourFB; + + // We'll keep track of and restore the current OpenGL line width, which we'll store in this FloatBuffer. + // Note: Although we only need a single float for this, LWJGL insists upon a minimum size of 16 floats. + private static FloatBuffer currentLineWidthFB; + + // ----- Non-Static Properties ----- + + // The FloatBuffer which will contain our vertex data - as we may load multiple different models this cannot be static + private FloatBuffer vertexFB; + + /** The actual Model associated with this FabrikModel3D. */ + private Model model; + + /** The float array storing the axis vertex (including colour) data. */ + private float[] modelData; + + /** + * The line width with which to draw the model in pixels. + * + * @default 1.0f + */ + private float mLineWidth = 1.0f; + + static { + mvpMatrixFB = Utils.createFloatBuffer(16); + colourFB = Utils.createFloatBuffer(16); + currentLineWidthFB = Utils.createFloatBuffer(16); + + // ----- Grid shader program setup ----- + + shaderProgram = new ShaderProgram(); + shaderProgram.initFromStrings(VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE); + + // ----- Grid shader attributes and uniforms ----- + + // Add the shader attributes and uniforms + shaderProgram.addAttribute("vertexLocation"); + shaderProgram.addUniform("mvpMatrix"); + shaderProgram.addUniform("colour"); + + // ----- Set up our Vertex Array Object (VAO) to hold the shader attributes ----- + + // Create a VAO and bind to it + vaoId = glGenVertexArrays(); + glBindVertexArray(vaoId); + + // ----- Vertex Buffer Object (VBO) ----- + + // Create a VBO and bind to it + vboId = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + + // Note: We do NOT copy the data into the buffer at this time - we do that on draw! + + // Vertex attribute configuration + glVertexAttribPointer(shaderProgram.attribute("vertexLocation"), // Vertex location attribute index + VERTEX_COMPONENTS, // Number of components per vertex + GL_FLOAT, // Data type + false, // Normalised? + VERTEX_COMPONENTS * Float.BYTES, // Stride + 0); // Offset + + // Unbind VBO + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Enable the vertex attributes + glEnableVertexAttribArray(shaderProgram.attribute("vertexLocation")); + + // Unbind VAO - all the buffer and attribute settings above will now be associated with our VAO + glBindVertexArray(0); + } + + /** + * Default constructor. + * + * @param modelFilename The filename of the model to load. + * @param lineWidth The width of the lines used to draw the model in pixels. + */ + // Note: width is along +/- x-axis, depth is along +/- z-axis, height is the location on + // the y-axis, numDivisions is how many lines to draw across each axis + public FabrikModel3D(String modelFilename, float lineWidth) + { + // Load the model, get the vertex data and put it into our vertex FloatBuffer + model = new Model(modelFilename); + modelData = model.getVertexFloatArray(); + vertexFB = Utils.createFloatBuffer(model.getNumVertices() * VERTEX_COMPONENTS); + + mLineWidth = lineWidth; + + } // End of constructor + + /** Private method to actually draw the model. */ + private void drawModel(float lineWidth, Colour4f colour, Mat4f mvpMatrix) + { + // Enable our shader program and bind to our VAO + shaderProgram.use(); + glBindVertexArray(vaoId); + + // Bind to our VBO so we can update the axis data for this particular axis object + glBindBuffer(GL_ARRAY_BUFFER, vboId); + + // Copy the data for this particular model into the vertex float buffer + // Note: The model is scaled to each individual bone length, hence the GL_DYNAMIC_DRAW performance hint. + vertexFB.put(modelData); + vertexFB.flip(); + glBufferData(GL_ARRAY_BUFFER, vertexFB, GL_DYNAMIC_DRAW); + + // Provide the mvp matrix uniform data + mvpMatrixFB.put( mvpMatrix.toArray() ); + mvpMatrixFB.flip(); + glUniformMatrix4fv(shaderProgram.uniform("mvpMatrix"), false, mvpMatrixFB); + + // Provide the model vertex colour data + colourFB.put( colour.toArray() ); + colourFB.flip(); + glUniform4fv(shaderProgram.uniform("colour"), colourFB); + + // Store the current GL_LINE_WIDTH + // IMPORTANT: We MUST allocate a minimum of 16 floats in our FloatBuffer in LWJGL, we CANNOT just get a FloatBuffer with 1 float! + // ALSO: glPushAttrib(GL_LINE_BIT); /* do stuff */ glPopAttrib(); should work instead of this in theory - but LWJGL fails with 'function not supported'. + glGetFloatv(GL_LINE_WIDTH, currentLineWidthFB); + + /// Set the GL_LINE_WIDTH to be the width requested, as passed to the constructor + glLineWidth(lineWidth); + + // Draw the model as lines + glDrawArrays( GL_LINES, 0, model.getNumVertices() ); + + // Reset the line width to the previous value + glLineWidth( currentLineWidthFB.get(0) ); + + // Unbind from our VBO + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Unbind from our VAO + glBindVertexArray(0); + + // Disable our shader program + shaderProgram.disable(); + } + + /** + * Draw a bone using the model loaded on this FabrikModel3D. + * + * @param bone The bone to draw. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the bone. + */ + public void drawBone(FabrikBone3D bone, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + // Clone the model and scale the clone to be twice as wide and deep, and scaled along the z-axis to match the bone length + Model modelCopy = Model.clone(model); + modelCopy.scale( 2.0f, 2.0f, bone.length() ); + + // Get our scaled model data + modelData = modelCopy.getVertexFloatArray(); + + // Construct a model matrix for this bone + Mat4f modelMatrix = new Mat4f( Mat3f.createRotationMatrix( bone.getDirectionUV().normalised() ), bone.getStartLocation() ); + + // Construct a ModelViewProjection and draw the model for this bone + Mat4f mvpMatrix = projectionMatrix.times(viewMatrix).times(modelMatrix); + this.drawModel(mLineWidth, colour, mvpMatrix); + } + + /** + * Draw a bone using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param bone The bone to draw. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawBone(FabrikBone3D bone, Mat4f viewMatrix, Mat4f projectionMatrix) + { + this.drawBone(bone, viewMatrix, projectionMatrix, Utils.WHITE); + } + + /** + * Draw a chain using the model loaded on this FabrikModel3D. + * + * @param chain The FabrikChain3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the model. + */ + public void drawChain(FabrikChain3D chain, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + int numBones = chain.getNumBones(); + for (int loop = 0; loop < numBones; ++loop) + { + this.drawBone( chain.getBone(loop), viewMatrix, projectionMatrix, colour ); + } + } + + /** + * Draw a chain using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param chain The FabrikChain3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawChain(FabrikChain3D chain, Mat4f viewMatrix, Mat4f projectionMatrix) + { + int numBones = chain.getNumBones(); + for (int loop = 0; loop < numBones; ++loop) + { + this.drawBone( chain.getBone(loop), viewMatrix, projectionMatrix, Utils.WHITE); + } + } + + /** + * Draw a structure using the model loaded on this FabrikModel3D. + * + * @param structure The FabrikStructure3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + * @param colour The colour of the lines used to draw the model. + */ + public void drawStructure(FabrikStructure3D structure, Mat4f viewMatrix, Mat4f projectionMatrix, Colour4f colour) + { + int numChains = structure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + this.drawChain( structure.getChain(loop), viewMatrix, projectionMatrix, colour ); + } + } + + /** + * Draw a structure using the model loaded on this FabrikModel3D using a default colour of white at full opacity. + * + * @param structure The FabrikStructure3D to draw the model as bones on. + * @param viewMatrix The view matrix, typically retrieved from the camera. + * @param projectionMatrix The projection matrix of our scene. + */ + public void drawStructure(FabrikStructure3D structure, Mat4f viewMatrix, Mat4f projectionMatrix) + { + int numChains = structure.getNumChains(); + for (int loop = 0; loop < numChains; ++loop) + { + this.drawChain( structure.getChain(loop), viewMatrix, projectionMatrix, Utils.WHITE); + } + } + + /** + * Line width property setter. + *

    + * Valid line widths are between 1.0f an 32.0f - values outside of this range will result + * in an IllegalArgumentException being thrown. + * + * @param lineWidth The width of the line used to draw this FabrikModel3D in pixels. + */ + void setLineWidth(float lineWidth) + { + Utils.validateLineWidth(lineWidth); + mLineWidth = lineWidth; + } + +} // End of FabrikModel3D class diff --git a/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java b/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java new file mode 100644 index 0000000000..b1b7c6b824 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/GuiDemoStructure.java @@ -0,0 +1,30 @@ +package org.myrobotlab.caliko; + +import org.myrobotlab.service.Caliko; + +import au.edu.federation.caliko.demo3d.CalikoDemoStructure3D; +import au.edu.federation.utils.Mat4f; + +public class GuiDemoStructure extends CalikoDemoStructure3D { + + private transient Caliko service; + protected String name; + + public GuiDemoStructure(Caliko service, String name) { + this.service = service; + this.name = name; + } + + @Override + public void setup() { + // TODO Auto-generated method stub + + } + + @Override + public void drawTarget(Mat4f mvpMatrix) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/org/myrobotlab/caliko/Model.java b/src/main/java/org/myrobotlab/caliko/Model.java new file mode 100644 index 0000000000..887934bdc6 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/Model.java @@ -0,0 +1,928 @@ +package org.myrobotlab.caliko; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import au.edu.federation.utils.Vec3f; +import au.edu.federation.utils.Vec3i; + +//TODO: This is pretty ineficient - change all the for..each loops to be normals loops to stop Java allocating memory. +//TODO: Also provide a proper copy-constructor rather than a clone method - they should do the same thing. + +/** + * A class to represent and load a 3D model in WaveFront .OBJ format. + *

    + * Vertices, normals, and faces with or without normal indices are supported. + *

    + * Models must be stored as triangles, not quads. + *

    + * There is no support for textures, texture coordinates, or grouped objects at this time. + * + * @author Al Lansley + * @version 0.5.1 - 07/01/2016 + */ +public class Model +{ + private static boolean VERBOSE = false; + private static final String NUMBER_OF_VERTICES_LOG = "Number of vertices in data array: %d (%d bytes)"; + private static final String NUMBER_OF_NORMALS_LOG = "Number of normals in data array: %d (%d bytes)"; + private static final String WRONG_COMPONENT_COUNT_LOG = "Found %s data with wrong component count at line number: %d - Skipping!"; + + // ---------- Private Properties ---------- + + // These are the models values as read from the file - they are not the final, consolidated model data + private List vertices = new ArrayList<>(); + private List normals = new ArrayList<>(); + private List normalIndices = new ArrayList<>(); + private List faces = new ArrayList<>(); + //ArrayList texCoords = new ArrayList(); + + // The vertexData and normalData arrays are the final consolidated data which we can draw with. + // + // Note: If the model has only vertices and/or normals, then the vertexData and normalData will be a direct 'unwrapped' + // version of the vertices and normals arrays. However, if we're using faces then there will likely be a lower number of + // vertices / normals as each vertex / normal may be used more than once in the model - the end result of this is that + // the vertexData and normalData will likely be larger than the 'as-read-from-file' vertices and normals arrays because + // of this duplication of values from the face data. + private List vertexData = new ArrayList<>(); + private List normalData = new ArrayList<>(); + //ArrayList texCoordData = new ArrayList(); + + // Counters to keep track of how many vertices, normals, normal indices, texture coordinates and faces + private int numVertices; + private int numNormals; + private int numNormalIndices; + private int numFaces; + //private int numTexCoords; + + // ---------- Public Methods ---------- + + /** Default constructor. */ + public Model() { } + + /** + * Constructor which creates a model object and loads the model from file. + * + * @param filename The file to load the model data from. + */ + public Model(String filename) { load(filename); } + + /** Enable verbose messages. */ + public static void enableVerbose() { VERBOSE = true; } + + /** Disable verbose messages. */ + public static void disableVerbose() { VERBOSE = false; } + + // Method provide create a deep copy of a Model so we can say copyOfMyModel.equals(myModel); + // Note: We only deep copy the vertexData and normalData arrays, not all the vectors! + public static Model clone(Model sourceModel) + { + // Create a new Model which we will clone across the data to from our source model + Model model = new Model(); + + // Update the counts of vertices and normals for the clone to match the source model + model.numVertices = sourceModel.getNumVertices(); + model.numNormals = sourceModel.getNumNormals(); + + // If the source model has vertices then copy them across to the clone... + if (model.numVertices > 0) + { + // For (foo IN bar) loops leak memory - go old-school + int vertCount = sourceModel.getNumVertices(); + for (int loop = 0; loop < vertCount; loop++) + { + //model.vertexData.add(f) + } + for ( Float f : sourceModel.getVertexData() ) { + model.vertexData.add(f); + } + } + else // ...or abort if we have no vertices to copy! + { + throw new RuntimeException("Model created using clone method has 0 vertices!"); + } + + // If the source model has normals then copy them across to the clone... + if (model.numNormals > 0) + { + for ( Float f : sourceModel.getNormalData() ) { + model.normalData.add(f); + } + } + else // ...or (potentially) inform the user if they are no normals. This is not necessarily a deal breaker though, so we don't abort. + { + if (VERBOSE) { + System.out.println( "Model created using clone method has 0 normals - continuing..."); + } + } + + // Display final status if appropriate + if (VERBOSE) { + System.out.println( "Model successfully cloned."); + } + + // Finally, return our cloned model + return model; + } + + /** + * Load a .OBJ model from file. + *

    + * By default, no feedback is provided on the model loading. If you wish to see what's going on + * internally, call Model.enableVerbose() before loading the model - this will display statistics + * about any vertices/normals/normal indices/faces found in the model, as well as any malformed data. + *

    + * If the model file does not contain any vertices then a RuntimeException is thrown. + * If the file cannot be found then a FileNotFoundException is thrown. + * If there was a file-system-type error when reading the file then an IOException is thrown. + * + * @param filename The name of the Wavefront .OBJ format model to load, include the path if necessary. + * @return Whether the file loaded successfully or not. Loading with warnings still counts as a + * successful load - if necessary enable verbose mode to ensure your model loaded cleanly. + */ + public boolean load(String filename) + { + // Load the model file + boolean modelLoadedCleanly = loadModel(filename); + + // Did we load the file without errors? + if (VERBOSE) + { + if (modelLoadedCleanly) { + System.out.println("Model loaded cleanly."); + } + else { + System.out.println("Model loaded with errors."); + } + } + + // Do we have vertices? If not then this is a non-recoverable error and we abort! + if ( hasVertices() ) + { + if (VERBOSE) + { + System.out.println("Model vertex count: " + getNumVertices() ); + if ( hasFaces() ) { + System.out.println( "Model face count: " + getNumFaces() ); + } + if ( hasNormals() ) { + System.out.println( "Model normal count: " + getNumNormals() ); + } + if ( hasNormalIndices() ) { + System.out.println( "Model normal index count: " + getNumNormalIndices() ); + } + } + } + else { throw new RuntimeException("Model has no vertices."); } + + // Transfer the loaded data in our vectors to the data arrays + setupData(); + + // Delete the vertices, normals, normalIndices and faces Lists as we now have the final + // data stored in the vertexData and normalData Lists. + vertices.clear(); + normals.clear(); + normalIndices.clear(); + faces.clear(); + vertices = null; + normals = null; + normalIndices = null; + faces = null; + + // Indicate that the model loaded successfully + return true; + } + + + // ---------- Getters ---------- + + /** + * Get the vertex data as a list of floats. + * + * @return The vertex data. + */ + public List getVertexData() { return vertexData; } + + /** + * Get the vertex normals as a list of floats. + * + * @return The vertex normal data. + */ + public List getNormalData() { return normalData; } + + /** + * Get the vertex data as a float array suitable for transfer into a FloatBuffer for drawing. + * + * @return The vertex data as a float array. + **/ + public float[] getVertexFloatArray() + { + // How many floats are there in our list of vertex data? + int numVertexFloats = vertexData.size(); + + // Create an array big enough to hold them + float[] vertexFloatArray = new float[numVertexFloats]; + + // Loop over each item in the list, setting it to the appropriate element in the array + for (int loop = 0; loop < numVertexFloats; loop++) { + vertexFloatArray[loop] = vertexData.get(loop); + } + + // Finally, return the float array + return vertexFloatArray; + } + + /** + * Get the vertex normal data as a float array suitable for transfer into a FloatBuffer for drawing. + * + * @return The vertex normal data as a float array. + */ + public float[] getNormalFloatArray() + { + // How many floats are there in our list of normal data? + int numNormalFloats = normalData.size(); + + // Create an array big enough to hold them + float[] normalFloatArray = new float[numNormalFloats]; + + // Loop over each item in the list, setting it to the appropriate element in the array + for (int loop = 0; loop < numNormalFloats; loop++) { + normalFloatArray[loop] = normalData.get(loop); + } + + // Finally, return the float array + return normalFloatArray; + } + + // Methods to get the sizes of various data arrays + // Note: Type.BYTES returns the size of on object of this type in Bytes, and we multiply + // by 3 because there are 3 components to a vertex (x/y/z), normal (s/t/p) and 3 vertexes comprising a face (i.e. triangle) + + /** + * Get the vertex data size in bytes. + * + * @return The vertex data size in bytes. + */ + public int getVertexDataSizeBytes() { return numVertices * 3 * Float.BYTES; } + + /** + * Get the vertex normal data size in bytes. + * + * @return The vertex normal data size in bytes. + */ + public int getNormalDataSizeBytes() { return numNormals * 3 * Float.BYTES; } + + /** + * Get the face data size in bytes. + * + * @return The face data size in bytes. + **/ + public int getFaceDataSizeBytes() { return numFaces * 3 * Integer.BYTES; } + + /** + * Get the number of vertices in this model. + * + * @return The number of vertices in this model. + */ + public int getNumVertices() { return numVertices; } + + /** + * Get the number of vertex normals in this model. + * + * @return The number of normals in this model. + */ + public int getNumNormals() { return numNormals; } + + /** Get the number of normal indices in this model. + * + * + * @return The number of normal indices in this model. + */ + public int getNumNormalIndices() { return numNormalIndices; } + + /** + * Get the number of faces in this model. + * + * @return The number of faces in this model. + */ + public int getNumFaces() { return numFaces; } + + // ---------- Utility Methods ---------- + + /** + * Scale this model uniformly along the x/y/z axes. + * + * @param scale The amount to scale the model. + **/ + public void scale(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; ++loop) { + vertexData.set(loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the X axis. + * + * @param scale The amount to scale the model on the X axis. + */ + public void scaleX(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the Y axis. + * + * @param scale The amount to scale the model on the Y axis. + */ + public void scaleY(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 1; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model on the Z axis. + * + * @param scale The amount to scale the model on the Z axis. + */ + public void scaleZ(float scale) + { + int numVerts = vertexData.size(); + for (int loop = 2; loop < numVerts; loop += 3) { + vertexData.set( loop, vertexData.get(loop) * scale); + } + } + + /** + * Scale this model by various amounts along separate axes. + * + * @param xScale The amount to scale the model on the X axis. + * @param yScale The amount to scale the model on the Y axis. + * @param zScale The amount to scale the model on the Z axis. + */ + public void scale(float xScale, float yScale, float zScale) + { + int numVerts = vertexData.size(); + for (int loop = 0; loop < numVerts; ++loop) + { + switch (loop % 3) + { + case 0: + vertexData.set(loop, vertexData.get(loop) * xScale); + break; + case 1: + vertexData.set(loop, vertexData.get(loop) * yScale); + break; + case 2: + vertexData.set(loop, vertexData.get(loop) * zScale); + break; + } + } + } + + /** Print out the vertices of this model. */ + public void printVertices() { + for (Vec3f v : vertices) { + System.out.println( "Vertex: " + v.toString() ); + } + } + + /** + * Print out the vertex normal data for this model. + *

    + * Note: This is the contents of the normals list, not the (possibly expanded) normalData array. + */ + public void printNormals() + { + for (Vec3f n : normals) { + System.out.println( "Normal: " + n.toString() ); + } + } + + /** + * Print the face data of this model. + *

    + * Note: Faces are ONE indexed, not zero indexed. + */ + public void printFaces() + { + for (Vec3i face : faces) { + System.out.println( "Face: " + face.toString() ); + } + } + + /** + * Print the vertex data of this model. + *

    + * Note: This is the contents of the vertexlData array which is actually used when drawing - and which may be + * different to the 'vertices' list when using faces (where vertices get re-used). + */ + public void printVertexData() + { + for (int loop = 0; loop < vertexData.size(); loop += 3) + { + System.out.println( "Vertex data element " + (loop / 3) + " is x: " + vertexData.get(loop) + "\ty: " + vertexData.get(loop+1) + "\tz: " + vertexData.get(loop+2) ); + } + } + + /** + * Print the normal data of this model. + *

    + * Note: This is the contents of the normalData array which is actually used when drawing - and which may be + * different to the 'normals' list when using faces (where normals get re-used). + */ + public void printNormalData() + { + for (int loop = 0; loop < normalData.size(); loop += 3) + { + System.out.println( "Normal data element " + (loop / 3) + " is x: " + normalData.get(loop) + "\ty: " + normalData.get(loop+1) + "\tz: " + normalData.get(loop+2)); + } + } + + // ---------- Private Methods ---------- + + // Method to read through the model file adding all vertices, faces and normals to our + // vertices, faces and normals vectors. + + // Note: This does NOT transfer the data into our vertexData, faceData or normalData arrays! + // That must be done as a separate step by calling setupData() after building up the + // arraylists with this method! + + // Also: This method does not decrement the face number of normal index by 1 (because .OBJ + // files start their counts at 1) to put them in a range starting from 0, that job + // is done in the setupData() method performed after calling this method! + private boolean loadModel(String filename) + { + // Initialise lists + vertices = new ArrayList<>(); + normals = new ArrayList<>(); + normalIndices = new ArrayList<>(); + faces = new ArrayList<>(); + //texCoords = new ArrayList(); + normalData = new ArrayList<>(); + //texCoordData = new ArrayList(); + + // Our vectors of attributes are initially empty + numVertices = 0; + numNormals = 0; + numNormalIndices = 0; + numFaces = 0; + //numTexCoords = 0; + + boolean loadedCleanly = true; + + // Counter to keep track of what line we're on + int lineCount = 0; + + // Use this for jar packaged resources + InputStream is = this.getClass().getResourceAsStream(filename); + try (BufferedReader br = new BufferedReader( new InputStreamReader(is) ) ) // This version loads from within jar archive, required for caliko-demo-jar-with-resources.jar + //try (BufferedReader br = new BufferedReader( new FileReader(filename) ) ) // Use this for loading from file in Eclipse or such + { + // We'll read through the file one line at a time - this will hold the current line we're working on + String line; + + // While there are lines left to read in the file... + while ((line = br.readLine()) != null) + { + ++lineCount; + + // If the line isn't empty (the 1 character is the carriage return), process it... + if (line.length() > 1) + { + // Split line on spaces into an array of strings.The + on the end of "\\s+" means 'do not accept + // blank entries' which can occur if you have two consecutive space characters (as happens when + // you export a .obj from 3ds max - you get "v 1.23 4.56 7.89" etc.) + String[] token = line.split("\\s+"); + + // If the first token is "v", then we're dealing with vertex data + if ( "v".equalsIgnoreCase(token[0]) ) + { + // As long as there's 4 tokens on the line... + if (token.length == 4) + { + // ...get the remaining 3 tokens as the x/y/z float values... + float x = Float.parseFloat(token[1]); + float y = Float.parseFloat(token[2]); + float z = Float.parseFloat(token[3]); + + // ... and push them into the vertices vector ... + vertices.add( new Vec3f(x, y, z) ); + + // .. then increase our vertex count. + numVertices++; + } + else // If we got vertex data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"vertex",lineCount); + } + + } + else if ( "vn".equalsIgnoreCase(token[0]) ) // If the first token is "vn", then we're dealing with a vertex normal + { + // As long as there's 4 tokens on the line... + if (token.length == 4) + { + // ...get the remaining 3 tokens as the x/y/z normal float values... + float normalX = Float.parseFloat(token[1]); + float normalY = Float.parseFloat(token[2]); + float normalZ = Float.parseFloat(token[3]); + + // ... and push them into the normals vector ... + normals.add( new Vec3f(normalX, normalY, normalZ) ); + + // .. then increase our normal count. + numNormals++; + } + else // If we got normal data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"normal",lineCount); + } + + } // End of vertex line parsing + + // If the first token is "f", then we're dealing with faces + // + // Note: Faces can be described in two ways - we can have data like 'f 123 456 789' which means that the face is comprised + // of vertex 123, vertex 456 and vertex 789. Or, we have have data like f 123//111 456//222 789//333 which means that + // the face is comprised of vertex 123 using normal 111, vertex 456 using normal 222 and vertex 789 using normal 333. + else if ( "f".equalsIgnoreCase(token[0]) ) + { + // Check if there's a double-slash in the line + int pos = line.indexOf("//"); + + // As long as there's four tokens on the line and they don't contain a "//"... + if ( (token.length == 4) && (pos == -1) ) + { + // ...get the face vertex numbers as ints ... + int v1 = Integer.parseInt(token[1]); + int v2 = Integer.parseInt(token[2]); + int v3 = Integer.parseInt(token[3]); + + // ... and push them into the faces vector ... + faces.add( new Vec3i(v1, v2, v3) ); + + // .. then increase our face count. + numFaces++; + } + else if ( (token.length == 4) && (pos != -1) ) // 4 tokens and found 'vertex//normal' notation? + { + // ----- Get the 1st of three tokens as a String ----- + + // Find where the double-slash starts in that token + int faceEndPos = token[1].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken1 = token[1].substring(0, faceEndPos); + + // Convert face token value to int + int ft1 = Integer.parseInt(faceToken1); + + // Mark the start of our next subtoken + int nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end of the token + String normalToken1 = token[1].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt1 = Integer.parseInt(normalToken1); + + // ----- Get the 2nd of three tokens as a String ----- + + // Find where the double-slash starts in that token + faceEndPos = token[2].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken2 = token[2].substring(0, faceEndPos); + + // Convert face token value to int + int ft2 = Integer.parseInt(faceToken2); + + // Mark the start of our next subtoken + nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end of the token + String normalToken2 = token[2].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt2 = Integer.parseInt(normalToken2); + + // ----- Get the 3rd of three tokens as a String ----- + + // Find where the double-slash starts in that token + faceEndPos = token[3].indexOf("//"); + + // Put sub-String from the start to the beginning of the double-slash into our subToken String + String faceToken3 = token[3].substring(0, faceEndPos); + + // Convert face token value to int + int ft3 = Integer.parseInt(faceToken3); + + // Mark the start of our next subtoken + nextTokenStartPos = faceEndPos + 2; + + // Copy from first character after the "//" to the end + String normalToken3 = token[3].substring(nextTokenStartPos); + + // Convert normal token value to int + int nt3 = Integer.parseInt(normalToken3); + + + // Finally, add the face to the faces array list and increment the face count... + faces.add( new Vec3i(ft1, ft2, ft3) ); + numFaces++; + + // ...and do the same for the normal indices and the normal index count. + normalIndices.add( new Vec3i(nt1, nt2, nt3) ); + numNormalIndices++; + + } + else // If we got face data without 3 components - whine! + { + loadedCleanly = false; + System.out.printf(WRONG_COMPONENT_COUNT_LOG,"face",lineCount); + } + + } // End of if token is "f" (i.e. face indices) + + // IMPLIED ELSE: If first token is something we don't recognise then we ignore it as a comment. + + } // End of line parsing section + + } // End of if line length > 1 check + + // No need to close the file ( i.e. br.close() ) - try-with-resources does that for us. + } + catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); } + catch (IOException ioe) { ioe.printStackTrace(); } + + // Return our boolean flag to say whether we loaded the model cleanly or not + return loadedCleanly; + } + + // ----- Helper Methods ----- + + /** + * Return whether or not this model contains vertex data. + * + * @return whether or not this model contains vertex data. + */ + public boolean hasVertices() { return (numVertices > 0); } + + /** + * Return whether or not this model contains face data. + * + * @return whether or not this model contains face data. + */ + public boolean hasFaces() { return (numFaces > 0); } + + /** + * Return whether or not this model contains normal data. + * + * @return whether or not this model contains normal data. + */ + public boolean hasNormals() { return (numNormals > 0); } + + /** + * Return whether or not this model contains normal index data. + * + * @return whether or not this model contains normal index data. + */ + public boolean hasNormalIndices() { return (numNormalIndices > 0); } + + /** + * Set up our plain arrays of floats for OpenGL to work with. + *

    + * Note: We CANNOT have size mismatches! The vertex count must match the + * normal count i.e. every vertex must have precisely ONE normal - no more, no less! + */ + private void setupData() + { + if (VERBOSE) { + System.out.println( "Setting up model data to draw as arrays."); + } + + // If we ONLY have vertex data, then transfer just that... + if ( ( hasVertices() ) && ( !hasFaces() ) && ( !hasNormals() ) ) + { + if (VERBOSE) { + System.out.println( "Model has no faces or normals. Transferring vertex data only."); + } + + // Reset the vertex count + numVertices = 0; + + // Transfer all vertices from the vertices vector to the vertexData array + for (Vec3f v : vertices) + { + vertexData.add( v.x ); + vertexData.add( v.y ); + vertexData.add( v.z ); + ++numVertices; + } + + // Print a summary of the vertex data + if (VERBOSE) { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + } + } + // If we have vertices AND faces BUT NOT normals... + else if ( ( hasVertices() ) && ( hasFaces() ) && ( !hasNormals() ) ) + { + if (VERBOSE) { + System.out.println("Model has vertices and faces, but no normals. Per-face normals will be generated.") ; + } + + // Create the vertexData and normalData arrays from the vector of faces + // Note: We generate the face normals ourselves. + int vertexCount = 0; + int normalCount = 0; + + for (Vec3i iv : faces) + { + // Get the numbers of the three vertices that this face is comprised of + int firstVertexNum = iv.x; + int secondVertexNum = iv.y; + int thirdVertexNum = iv.z; + + // Now that we have the vertex numbers, we need to get the actual vertices + // Note: We subtract 1 from the number of the vertex because faces start at + // face number 1 in the .OBJ format, while in our code the first vertex + // will be at location zero. + Vec3f faceVert1 = vertices.get(firstVertexNum - 1); + Vec3f faceVert2 = vertices.get(secondVertexNum - 1); + Vec3f faceVert3 = vertices.get(thirdVertexNum - 1); + + // Now that we have the 3 vertices, we need to calculate the normal of the face + // formed by these vertices... + + // Convert this vertex data into a pure form + Vec3f v1 = faceVert2.minus(faceVert1); + Vec3f v2 = faceVert3.minus(faceVert1); + + // Generate the normal as the cross product and normalise it + Vec3f normal = v1.cross(v2); + Vec3f normalisedNormal = normal.normalise(); + + // Put the vertex data into our vertexData array + vertexData.add( faceVert1.x ); + vertexData.add( faceVert1.y ); + vertexData.add( faceVert1.z ); + vertexCount++; + + vertexData.add( faceVert2.x ); + vertexData.add( faceVert2.y ); + vertexData.add( faceVert2.z ); + vertexCount++; + + vertexData.add( faceVert3.x ); + vertexData.add( faceVert3.y ); + vertexData.add( faceVert3.z ); + vertexCount++; + + // Put the normal data into our normalData array + // + // Note: we put the same normal into the normalData array for each of the 3 vertices comprising the face! + // This gives use a 'faceted' looking model, but is easy! You could try to calculate an interpolated + // normal based on surrounding normals, but that's not a trivial task (although 3ds max will do it for you + // if you load up the model and export it with normals!) + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + normalData.add( normalisedNormal.x ); + normalData.add( normalisedNormal.y ); + normalData.add( normalisedNormal.z ); + normalCount++; + + } // End of loop iterating over the model faces + + numVertices = vertexCount; + numNormals = normalCount; + + if (VERBOSE) + { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + System.out.printf( NUMBER_OF_NORMALS_LOG, numNormals, getNormalDataSizeBytes()); + } + } + // If we have vertices AND faces AND normals AND normalIndices... + else if ( ( hasVertices() ) && ( hasFaces() ) && ( hasNormals() ) && ( hasNormalIndices() ) ) + { + if (VERBOSE) + { + System.out.println("Model has vertices, faces, normals & normal indices. Transferring data."); + } + + //FIXME: Change this to use the numVertices and numNormals directly - I don't see a reason to use separate vertexCount and normalCount vars... + + int vertexCount = 0; + int normalCount = 0; + + // Look up each vertex specified by each face and add the vertex data to the vertexData array + for (Vec3i iv : faces) + { + // Get the numbers of the three vertices that this face is comprised of + int firstVertexNum = iv.x; + int secondVertexNum = iv.y; + int thirdVertexNum = iv.z; + + // Now that we have the vertex numbers, we need to get the actual vertices + // Note: We subtract 1 from the number of the vertex because faces start at + // face number 1 in the .oBJ format, while in our code the first vertex + // will be at location zero. + Vec3f faceVert1 = vertices.get(firstVertexNum - 1); + Vec3f faceVert2 = vertices.get(secondVertexNum - 1); + Vec3f faceVert3 = vertices.get(thirdVertexNum - 1); + + // Put the vertex data into our vertexData array + vertexData.add( faceVert1.x ); + vertexData.add( faceVert1.y ); + vertexData.add( faceVert1.z ); + ++vertexCount; + + vertexData.add( faceVert2.x ); + vertexData.add( faceVert2.y ); + vertexData.add( faceVert2.z ); + ++vertexCount; + + vertexData.add( faceVert3.x ); + vertexData.add( faceVert3.y ); + vertexData.add( faceVert3.z ); + ++vertexCount; + } + + // Look up each normal specified by each normal index and add the normal data to the normalData array + for (Vec3i normInd : normalIndices) + { + // Get the numbers of the three normals that this face uses + int firstNormalNum = normInd.x; + int secondNormalNum = normInd.y; + int thirdNormalNum = normInd.z; + + // Now that we have the normal index numbers, we need to get the actual normals + // Note: We subtract 1 from the number of the normal because normals start at + // number 1 in the .obJ format, while in our code the first vertex + // will be at location zero. + Vec3f normal1 = normals.get(firstNormalNum - 1); + Vec3f normal2 = normals.get(secondNormalNum - 1); + Vec3f normal3 = normals.get(thirdNormalNum - 1); + + // Put the normal data into our normalData array + normalData.add( normal1.x ); + normalData.add( normal1.y ); + normalData.add( normal1.z ); + normalCount++; + + normalData.add( normal2.x ); + normalData.add( normal2.y ); + normalData.add( normal2.z ); + normalCount++; + + normalData.add( normal3.x ); + normalData.add( normal3.y ); + normalData.add( normal3.z ); + normalCount++; + + } // End of loop iterating over the model faces + + numVertices = vertexCount; + numNormals = normalCount; + + if (VERBOSE) + { + System.out.printf( NUMBER_OF_VERTICES_LOG, numVertices, getVertexDataSizeBytes()); + System.out.printf( NUMBER_OF_NORMALS_LOG, numNormals, getNormalDataSizeBytes()); + } + } + else + { + System.out.println("Something bad happened in Model.setupData() =("); + } + + } // End of setupData method + +} // End of Model class diff --git a/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java b/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java new file mode 100644 index 0000000000..9582c6adf7 --- /dev/null +++ b/src/main/java/org/myrobotlab/caliko/OpenGLWindow.java @@ -0,0 +1,505 @@ +package org.myrobotlab.caliko; + +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; +import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MINOR; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_DISABLED; +import static org.lwjgl.glfw.GLFW.GLFW_CURSOR_NORMAL; +import static org.lwjgl.glfw.GLFW.GLFW_FOCUSED; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_A; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_C; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_D; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_DOWN; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_F; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_L; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_LEFT; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_M; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_P; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_R; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_RIGHT; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_S; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_SPACE; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_UP; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_W; +import static org.lwjgl.glfw.GLFW.GLFW_KEY_X; +import static org.lwjgl.glfw.GLFW.GLFW_MOUSE_BUTTON_1; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_CORE_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_PRESS; +import static org.lwjgl.glfw.GLFW.GLFW_RELEASE; +import static org.lwjgl.glfw.GLFW.GLFW_REPEAT; +import static org.lwjgl.glfw.GLFW.GLFW_RESIZABLE; +import static org.lwjgl.glfw.GLFW.GLFW_SAMPLES; +import static org.lwjgl.glfw.GLFW.GLFW_TRUE; +import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE; +import static org.lwjgl.glfw.GLFW.glfwCreateWindow; +import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; +import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor; +import static org.lwjgl.glfw.GLFW.glfwGetVideoMode; +import static org.lwjgl.glfw.GLFW.glfwInit; +import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; +import static org.lwjgl.glfw.GLFW.glfwSetCursorPos; +import static org.lwjgl.glfw.GLFW.glfwSetCursorPosCallback; +import static org.lwjgl.glfw.GLFW.glfwSetErrorCallback; +import static org.lwjgl.glfw.GLFW.glfwSetInputMode; +import static org.lwjgl.glfw.GLFW.glfwSetKeyCallback; +import static org.lwjgl.glfw.GLFW.glfwSetMouseButtonCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowPos; +import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose; +import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowTitle; +import static org.lwjgl.glfw.GLFW.glfwShowWindow; +import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; +import static org.lwjgl.glfw.GLFW.glfwSwapInterval; +import static org.lwjgl.glfw.GLFW.glfwTerminate; +import static org.lwjgl.glfw.GLFW.glfwWindowHint; +import static org.lwjgl.opengl.GL11.GL_BLEND; +import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; +import static org.lwjgl.opengl.GL11.GL_LEQUAL; +import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA; +import static org.lwjgl.opengl.GL11.glBlendFunc; +import static org.lwjgl.opengl.GL11.glClearColor; +import static org.lwjgl.opengl.GL11.glClearDepth; +import static org.lwjgl.opengl.GL11.glDepthFunc; +import static org.lwjgl.opengl.GL11.glEnable; +import static org.lwjgl.opengl.GL11.glViewport; +import static org.lwjgl.system.MemoryUtil.NULL; + +import org.lwjgl.glfw.GLFWCursorPosCallback; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWKeyCallback; +import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.glfw.GLFWWindowSizeCallback; +import org.lwjgl.opengl.GL; +import org.myrobotlab.service.Caliko; +import org.myrobotlab.service.config.CalikoConfig; + +import au.edu.federation.caliko.visualisation.Axis; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.caliko.visualisation.Grid; +//import au.edu.federation.caliko.demo2d.CalikoDemoStructure2DFactory.CalikoDemoStructure2DEnum; +//import au.edu.federation.caliko.demo3d.CalikoDemoStructure3DFactory.CalikoDemoStructure3DEnum; +import au.edu.federation.utils.Mat4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec2f; +import au.edu.federation.utils.Vec3f; + +/** + * Class to set up an OpenGL window. + * + * @author Al Lansley + * @version 0.3 - 07/12/2015 + */ +public class OpenGLWindow +{ + + // Mouse cursor locations in screen-space and world-space + public Vec2f screenSpaceMousePos = null; + public Vec2f worldSpaceMousePos = new Vec2f(); + + // Window properties + long mWindowId; + int mWindowWidth; + int mWindowHeight; + float mAspectRatio; + + // Matrices + Mat4f mProjectionMatrix; + Mat4f mModelMatrix = new Mat4f(1.0f); + Mat4f mMvpMatrix = new Mat4f(); + + // Matrix properties + boolean mOrthographicProjection; // Use orthographic projection? If false, we use a standard perspective projection + float mVertFoVDegs; + float mZNear; + float mZFar; + float mOrthoExtent; + + // We need to strongly reference callback instances so that they don't get garbage collected. + private GLFWErrorCallback errorCallback; + private GLFWKeyCallback keyCallback; + private GLFWWindowSizeCallback windowSizeCallback; + private GLFWMouseButtonCallback mouseButtonCallback; + private GLFWCursorPosCallback cursorPosCallback; + + CalikoConfig config = null; + + Caliko service = null; + public Application application; + + // Constructor + public OpenGLWindow(Application application, Caliko service, int windowWidth, int windowHeight, float vertFoVDegs, float zNear, float zFar, float orthoExtent) + { + this.service = service; + this.application = application; + + // Set properties and create the projection matrix + mWindowWidth = windowWidth <= 0 ? 1 : windowWidth; + mWindowHeight = windowHeight <= 0 ? 1 : windowHeight; + mAspectRatio = (float)mWindowWidth / (float)mWindowHeight; + + mVertFoVDegs = vertFoVDegs; + mZNear = zNear; + mZFar = zFar; + mOrthoExtent = orthoExtent; + + config = service.getConfig(); + + screenSpaceMousePos = new Vec2f(config.windowWidth / 2.0f, config.windowHeight / 2.0f); + +// if (config.use3dDemo) + + mOrthographicProjection = false; + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + +// else +// { +// mOrthographicProjection = true; +// mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); +// } + + // Setup the error callback to output to System.err + glfwSetErrorCallback(errorCallback = GLFWErrorCallback.createPrint(System.err)); + + // Initialize GLFW. Most GLFW functions will not work before doing this. + if ( !glfwInit() ) { throw new IllegalStateException("Unable to initialize GLFW"); } + + // ----- Specify window hints ----- + // Note: Window hints must be specified after glfwInit() (which resets them) and before glfwCreateWindow where the context is created. + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // Request OpenGL version 3.3 (the minimum we can get away with) + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // We want a core profile without any deprecated functionality... + //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // ...however we do NOT want a forward compatible profile as they've removed line widths! + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // We want the window to be resizable + glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE); // We want the window to be visible (false makes it hidden after creation) + glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE); // We want the window to take focus on creation + glfwWindowHint(GLFW_SAMPLES, 4); // Ask for 4x anti-aliasing (this doesn't mean we'll get it, though) + + // Create the window + mWindowId = glfwCreateWindow(mWindowWidth, mWindowHeight, "LWJGL3 Test", NULL, NULL); + if (mWindowId == NULL) { throw new RuntimeException("Failed to create the GLFW window"); } + + // Get the resolution of the primary monitor + GLFWVidMode vidmode = glfwGetVideoMode( glfwGetPrimaryMonitor() ); + int windowHorizOffset = (vidmode.width() - mWindowWidth) / 2; + int windowVertOffset = (vidmode.height() - mWindowHeight) / 2; + + glfwSetWindowPos(mWindowId, windowHorizOffset, windowVertOffset); // Center our window + glfwMakeContextCurrent(mWindowId); // Make the OpenGL context current + glfwSwapInterval(1); // Swap buffers every frame (i.e. enable vSync) + + // This line is critical for LWJGL's interoperation with GLFW's OpenGL context, or any context that is managed externally. + // LWJGL detects the context that is current in the current thread, creates the ContextCapabilities instance and makes + // the OpenGL bindings available for use. + glfwMakeContextCurrent(mWindowId); + + // Enumerate the capabilities of the current OpenGL context, loading forward compatible capabilities + GL.createCapabilities(true); + + // Setup our keyboard, mouse and window resize callback functions + setupCallbacks(); + + // ---------- OpenGL settings ----------- + + // Set the clear color + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + + // Specify the size of the viewport. Params: xOrigin, yOrigin, width, height + glViewport(0, 0, mWindowWidth, mWindowHeight); + + // Enable depth testing + glDepthFunc(GL_LEQUAL); + glEnable(GL_DEPTH_TEST); + + // When we clear the depth buffer, we'll clear the entire buffer + glClearDepth(1.0f); + + // Enable blending to use alpha channels + // Note: blending must be enabled to use transparency / alpha values in our fragment shaders. + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + glfwShowWindow(mWindowId); // Make the window visible + } + + // Constructor with some sensible projection matrix values hard-coded + public OpenGLWindow(Application application, Caliko service, int width, int height) { this(application, service, width, height, 35.0f, 1.0f, 5000.0f, 120.0f); } + + /** Return a calculated ModelViewProjection matrix. + *

    + * This MVP matrix is the result of multiplying the projection matrix by the view matrix obtained from the camera, and + * as such is really a ProjectionView matrix or 'identity MVP', however you'd like to term it. + * + * If you want a MVP matrix specific to your model, simply multiply this matrix by your desired model matrix to create + * a MVP matrix specific to your model. + * + * @return A calculate ModelViewProjection matrix. + */ + public Mat4f getMvpMatrix() { return mProjectionMatrix.times( service.getCamera().getViewMatrix() ); } + + /** + * Return the projection matrix. + * + * @return The projection matrix. + */ + public Mat4f getProjectionMatrix() { return mProjectionMatrix; } + + /** Swap the front and back buffers to update the display. */ + public void swapBuffers() { glfwSwapBuffers(mWindowId); } + + /** + * Set the window title to the specified String argument. + * + * @param title The String that will be used as the title of the window. + */ + public void setWindowTitle(String title) { glfwSetWindowTitle(mWindowId, title); } + + /** Destroy the window, finish up glfw and release all callback methods. */ + public void cleanup() + { + // Free the window callbacks and destroy the window + //glfwFreeCallbacks(mWindowId); + cursorPosCallback.close(); + mouseButtonCallback.close(); + windowSizeCallback.close(); + keyCallback.close(); + + glfwDestroyWindow(mWindowId); + + // Terminate GLFW and free the error callback + glfwTerminate(); + glfwSetErrorCallback(null).free(); + } + + // Setup keyboard, mouse cursor, mouse button and window resize callback methods. + private void setupCallbacks() + { + // Key callback + glfwSetKeyCallback(mWindowId, keyCallback = GLFWKeyCallback.create( (long window, int key, int scancode, int action, int mods) -> + { + if (action == GLFW_PRESS) + { + switch (key) + { +// // Setup demos +// case GLFW_KEY_RIGHT: +// if (config.use3dDemo) +// { +// if (config.demoNumber < CalikoDemoStructure3DEnum.values().length) { config.demoNumber++; } +// } +// else // 2D Demo mode +// { +// if (config.demoNumber < CalikoDemoStructure2DEnum.values().length) { config.demoNumber++; } +// } +// config.demo.setup(config.demoNumber); +// break; +// case GLFW_KEY_LEFT: +// if (config.demoNumber > 1) { config.demoNumber--; } +// config.demo.setup(config.demoNumber); +// break; +// +// // Toggle fixed base mode +// case GLFW_KEY_F: +// config.fixedBaseMode = !config.fixedBaseMode; +// config.demo.setFixedBaseMode(config.fixedBaseMode); +// break; +// // Toggle rotating bases +// case GLFW_KEY_R: +// config.rotateBasesMode = !config.rotateBasesMode; +// break; + + // Various drawing options + case GLFW_KEY_C: + config.drawConstraints = !config.drawConstraints; + break; + case GLFW_KEY_L: + config.drawLines = !config.drawLines; + break; + case GLFW_KEY_M: + config.drawModels = !config.drawModels; + break; + case GLFW_KEY_P: + mOrthographicProjection = !mOrthographicProjection; + if (mOrthographicProjection) + { + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + } + else + { + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + } + break; + case GLFW_KEY_X: + config.drawAxes = !config.drawAxes; + break; + + // Camera controls + case GLFW_KEY_W: + case GLFW_KEY_S: + case GLFW_KEY_A: + case GLFW_KEY_D: + // if (config.use3dDemo) { config.demo.handleCameraMovement(key, action); } + break; + + // Close the window + case GLFW_KEY_ESCAPE: + glfwSetWindowShouldClose(window, true); + break; + + // Cycle through / switch between 2D and 3D demos with the up and down cursors + case GLFW_KEY_UP: + case GLFW_KEY_DOWN: + config.use3dDemo = !config.use3dDemo; + config.demoNumber = 1; + + // Viewing 2D demos? + if (!config.use3dDemo) + { + mOrthographicProjection = true; + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + // config.demo = new CalikoDemo2D(config.demoNumber); + } + else // Viewing 3D demos + { + mOrthographicProjection = false; + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + // config.demo = new CalikoDemo3D(config.demoNumber); + } + break; + + // Dynamic add/remove bones for first demo +// case GLFW_KEY_COMMA: +// if (config.demoNumber == 1 && config.structure.getChain(0).getNumBones() > 1) +// { +// config.structure.getChain(0).removeBone(0); +// } +// break; +// case GLFW_KEY_PERIOD: +// if (config.demoNumber == 1) +// { +// config.structure.getChain(0).addConsecutiveBone(config.X_AXIS, config.defaultBoneLength); +// } +// break; + + case GLFW_KEY_SPACE: + application.paused = !application.paused; + break; + + } // End of switch + + } + else if (action == GLFW_REPEAT || action == GLFW_RELEASE) // Camera must also handle repeat or release actions + { + switch (key) + { + case GLFW_KEY_W: + case GLFW_KEY_S: + case GLFW_KEY_A: + case GLFW_KEY_D: + //if (config.use3dDemo) { config.demo.handleCameraMovement(key, action); } + break; + } + } + })); + + // Mouse cursor position callback + glfwSetCursorPosCallback(mWindowId, cursorPosCallback = GLFWCursorPosCallback.create( (long windowId, double mouseX, double mouseY) -> + { + // Update the screen space mouse location + screenSpaceMousePos.set( (float)mouseX, (float)mouseY ); + + // If we're holding down the LMB, then... + if (config.leftMouseButtonDown) + { + // ...in the 3D demo we update the camera look direction... + if (config.use3dDemo) + { + service.getCamera().handleMouseMove(mouseX, mouseY); + } + else // ...while in the 2D demo we update the 2D target. + { + // Convert the mouse position in screen-space coordinates to our orthographic world-space coordinates +// worldSpaceMousePos.set( Utils.convertRange(screenSpaceMousePos.x, 0.0f, mWindowWidth, -mOrthoExtent, mOrthoExtent), +// -Utils.convertRange(screenSpaceMousePos.y, 0.0f, mWindowHeight, -mOrthoExtent, mOrthoExtent) ); +// +// CalikoDemo2D.mStructure.solveForTarget(worldSpaceMousePos); + } + } + })); + + // Mouse button callback + glfwSetMouseButtonCallback(mWindowId, mouseButtonCallback = GLFWMouseButtonCallback.create( (long windowId, int button, int action, int mods) -> + { + // If the left mouse button was the button that invoked the callback... + if (button == GLFW_MOUSE_BUTTON_1) + { + // ...then set the LMB status flag accordingly + // Note: We cannot simply toggle the flag here as double-clicking the title bar to fullscreen the window confuses it and we + // then end up mouselook-ing without the LMB being held down! + if (action == GLFW_PRESS) { config.leftMouseButtonDown = true; } else { config.leftMouseButtonDown = false; } + + if (config.use3dDemo) + { + // Immediately set the cursor position to the centre of the screen so our view doesn't "jump" on first cursor position change + glfwSetCursorPos(windowId, ((double)mWindowWidth / 2), ((double)mWindowHeight / 2) ); + + switch (action) + { + case GLFW_PRESS: + // Make the mouse cursor hidden and put it into a 'virtual' mode where its values are not limited + glfwSetInputMode(mWindowId, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + break; + + case GLFW_RELEASE: + // Restore the mouse cursor to normal and reset the camera last cursor position to be the middle of the window + glfwSetInputMode(windowId, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + service.getCamera().resetLastCursorPosition(); + break; + } + } + else + { + // Convert the mouse position in screen-space coordinates to our orthographic world-space coordinates + worldSpaceMousePos.set( Utils.convertRange(screenSpaceMousePos.x, 0.0f, mWindowWidth, -mOrthoExtent, mOrthoExtent), + -Utils.convertRange(screenSpaceMousePos.y, 0.0f, mWindowHeight, -mOrthoExtent, mOrthoExtent) ); + + // CalikoDemo2D.mStructure.solveForTarget(worldSpaceMousePos); + } + + // Nothing needs be done in 2D demo mode - the config.leftMouseButtonDown flag plus the mouse cursor handler take care of it. + } + })); + + // Window size callback + glfwSetWindowSizeCallback(mWindowId, windowSizeCallback = GLFWWindowSizeCallback.create( (long windowId, int windowWidth, int windowHeight) -> + { + // Update our window width and height and recalculate the aspect ratio + if (windowWidth <= 0) { windowWidth = 1; } + if (windowHeight <= 0) { windowHeight = 1; } + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mAspectRatio = (float)mWindowWidth / (float)mWindowHeight; + + // Let our camera know about the new size so it can correctly recentre the mouse cursor + service.getCamera().updateWindowSize(windowWidth, windowHeight); + + // Update our viewport + glViewport(0, 0, mWindowWidth, mWindowHeight); + + // Recalculate our projection matrix + if (mOrthographicProjection) + { + mProjectionMatrix = Mat4f.createOrthographicProjectionMatrix(-mOrthoExtent, mOrthoExtent, mOrthoExtent, -mOrthoExtent, mZNear, mZFar); + } + else + { + mProjectionMatrix = Mat4f.createPerspectiveProjectionMatrix(mVertFoVDegs, mAspectRatio, mZNear, mZFar); + } + })); + + } // End of setupCallbacks method + +} // End of OpenGLWindow class \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java index 03b8c251b9..6fd3e43b13 100644 --- a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java +++ b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java @@ -235,7 +235,10 @@ public boolean moveToGoal(Point goal) { int numSteps = 0; double iterStep = 0.05; // we're in millimeters.. - double errorThreshold = 2.0; + double errorThreshold = 20.0; + + maxIterations = 1000; + // what's the current point while (true) { numSteps++; diff --git a/src/main/java/org/myrobotlab/service/Caliko.java b/src/main/java/org/myrobotlab/service/Caliko.java new file mode 100644 index 0000000000..01381f1161 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/Caliko.java @@ -0,0 +1,401 @@ +package org.myrobotlab.service; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.caliko.Application; +import org.myrobotlab.framework.Service; +import org.myrobotlab.logging.Level; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.logging.LoggingFactory; +import org.myrobotlab.service.config.CalikoConfig; +import org.slf4j.Logger; + +import au.edu.federation.caliko.FabrikBone3D; +import au.edu.federation.caliko.FabrikChain3D; +import au.edu.federation.caliko.FabrikChain3D.BaseboneConstraintType3D; +import au.edu.federation.caliko.FabrikJoint3D.JointType; +import au.edu.federation.caliko.FabrikStructure3D; +import au.edu.federation.caliko.visualisation.Camera; +import au.edu.federation.utils.Colour4f; +import au.edu.federation.utils.Utils; +import au.edu.federation.utils.Vec3f; + +public class Caliko extends Service { + + private class WindowWorker extends Thread { + + Application application; + String name; + Caliko service; + + public WindowWorker(Caliko service, String name) { + super(String.format("%s.WindowWorker.%s", service.getName(), name)); + this.name = name; + this.service = service; + } + + public void run() { + try { + application = new Application(service); + } catch (Exception e) { + error(e); + shutdown(); + } + } + + public void shutdown() { + application.running = false; + windows.remove(name); + application.window.cleanup(); + } + + } + + public final static Logger log = LoggerFactory.getLogger(Caliko.class); + + private static final long serialVersionUID = 1L; + +// public static final Vec3f X_AXIS = new Vec3f(1.0f, 0.0f, 0.0f); +// +// public static final Vec3f Y_AXIS = new Vec3f(0.0f, 1.0f, 0.0f); +// +// public static final Vec3f Z_AXIS = new Vec3f(0.0f, 0.0f, 1.0f); +// +// public static final Vec3f NEG_X_AXIS = new Vec3f(-1.0f, -0.0f, -0.0f); +// +// public static final Vec3f NEG_Y_AXIS = new Vec3f(-0.0f, -1.0f, -0.0f); +// +// public static final Vec3f NEG_Z_AXIS = new Vec3f(-0.0f, -0.0f, -1.0f); + + + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + // identical to command line start + // Runtime.startConfig("inmoov2"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + + Caliko caliko = (Caliko) Runtime.start("caliko", "Caliko"); + caliko.test(); + + boolean done = true; + if (done) + return; + + Runtime.start("webgui", "WebGui"); + caliko.test(); + // Runtime.start("webgui", "WebGui"); + log.info("here"); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + private transient Map chains = new HashMap<>(); + + private transient FabrikStructure3D structure; + + protected int windowHeight = 600; + + transient private Map windows = new HashMap<>(); + + protected int windowWidth = 800; + + private transient Camera camera = new Camera(new Vec3f(0.0f, 00.0f, 150.0f), new Vec3f(), windowWidth, windowHeight); + + public Caliko(String n, String id) { + super(n, id); + structure = new FabrikStructure3D(n); + } + + public void addBaseBone(float startX, float startY, float startZ, float endX, float endY, float endZ) { + addChain("default"); + addBone("default", startX, startY, startZ, endX, endY, endX, "-x", 0.0f, "red"); + } + + public void addBone(String chainName, float startX, float startY, float startZ, float endX, float endY, float endZ, String axis, float constraints, String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikBone3D basebone = new FabrikBone3D(new Vec3f(startX, startY, startZ), new Vec3f(endX, endY, endZ)); + basebone.setColour(getColor(color)); + FabrikChain3D chain = chains.get(chainName); + chain.addBone(basebone); + // TODO set type + chain.setRotorBaseboneConstraint(BaseboneConstraintType3D.GLOBAL_ROTOR, getAxis(axis), constraints); + } + + public void addChain(String name) { + if (chains.containsKey(name)) { + error("chain %s already exists", name); + return; + } + FabrikChain3D chain = new FabrikChain3D(); + structure.addChain(chain); + chains.put(name, chain); + } + + public void addFreelyRotatingHingedBone(String directionUV, float length, String hingeRotationAxis, String color) { + addFreelyRotatingHingedBone("default", directionUV, length, hingeRotationAxis, color); + } + + /** + * Add a consecutive hinge constrained bone to the end of this chain. The bone + * may rotate freely about the hinge axis. + *

    + * The bone will be drawn with a default colour of white. + *

    + * This method can only be used when the IK chain contains a basebone, as + * without it we do not have a start location for this bone (i.e. the end + * location of the previous bone). + *

    + * If this method is executed on a chain which does not contain a basebone + * then a RuntimeException is thrown. If this method is provided with a + * direction unit vector of zero, then an IllegalArgumentException is thrown. + * If the joint type requested is not JointType.LOCAL_HINGE or + * JointType.GLOBAL_HINGE then an IllegalArgumentException is thrown. If this + * method is provided with a hinge rotation axis unit vector of zero, then an + * IllegalArgumentException is thrown. + * + * @param directionUV + * The initial direction of the new bone. + * @param length + * The length of the new bone. + * @param jointType + * The type of hinge joint to be used - either JointType.LOCAL or + * JointType.GLOBAL. + * @param hingeRotationAxis + * The axis about which the hinge joint freely rotates. + * @param colour + * The colour to draw the bone. + */ + public void addFreelyRotatingHingedBone(String chainName, String directionUV, float length, String hingeRotationAxis, String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikChain3D chain = chains.get(chainName); + chain.addConsecutiveFreelyRotatingHingedBone(getAxis(directionUV), length, JointType.LOCAL_HINGE, getAxis(hingeRotationAxis), getColor(color)); + } + + public void addHingeBone(String directionUV, float length, String hingeRotationAxis, float clockwiseDegs, float anticlockwiseDegs, String hingeReferenceAxis, String color) { + addHingeBone("default", directionUV, length, hingeRotationAxis, clockwiseDegs, anticlockwiseDegs, hingeReferenceAxis, color); + } + + /** + * Add a consecutive hinge constrained bone to the end of this IK chain. + *

    + * The hinge type may be a global hinge where the rotation axis is specified + * in world-space, or a local hinge, where the rotation axis is relative to + * the previous bone in the chain. + *

    + * If this method is executed on a chain which does not contain a basebone + * then a RuntimeException is thrown. If this method is provided with bone + * direction or hinge constraint axis of zero then an IllegalArgumentException + * is thrown. If the joint type requested is not LOCAL_HINGE or GLOBAL_HINGE + * then an IllegalArgumentException is thrown. + * + * @param chainName The name of chain + * @param directionUV + * The initial direction of the new bone. + * @param length + * The length of the new bone. + * @param jointType + * The joint type of the new bone. + * @param hingeRotationAxis + * The axis about which the hinge rotates. + * @param clockwiseDegs + * The clockwise constraint angle in degrees. + * @param anticlockwiseDegs + * The anticlockwise constraint angle in degrees. + * @param hingeReferenceAxis + * The axis about which any clockwise/anticlockwise rotation + * constraints are enforced. + * @param colour + * The colour to draw the bone. + */ + public void addHingeBone(String chainName, String directionUV, float length, String hingeRotationAxis, float clockwiseDegs, float anticlockwiseDegs, String hingeReferenceAxis, + String color) { + if (!chains.containsKey(chainName)) { + error("%s chain does not exist", chainName); + } + FabrikChain3D chain = chains.get(chainName); + chain.addConsecutiveHingedBone(getAxis(directionUV), length, JointType.LOCAL_HINGE, getAxis(hingeRotationAxis), clockwiseDegs, anticlockwiseDegs, getAxis(hingeReferenceAxis), + getColor(color)); + } + + Vec3f getAxis(String axis) { + if (axis == null) { + return null; + } + axis = axis.toLowerCase(); + if ("x".equals(axis)) { + return new Vec3f(1.0f, 0.0f, 0.0f); + } else if ("-x".equals(axis)) { + return new Vec3f(-1.0f, -0.0f, -0.0f); + } else if ("y".equals(axis)) { + return new Vec3f(0.0f, 1.0f, 0.0f); + } else if ("-y".equals(axis)) { + return new Vec3f(-0.0f, -1.0f, -0.0f); + } else if ("z".equals(axis)) { + return new Vec3f(0.0f, 0.0f, 1.0f); + } else if ("-z".equals(axis)) { + return new Vec3f(-0.0f, -0.0f, -1.0f); + } else { + error("axis %s not found", axis); + return null; + } + } + + public Camera getCamera() { + return camera; + } + + Colour4f getColor(String color) { + if (color == null) { + return Utils.GREY; + } + color = color.toLowerCase(); + if ("red".equals(color)) { + return Utils.RED; + } + if ("green".equals(color)) { + return Utils.GREEN; + } + if ("blue".equals(color)) { + return Utils.BLUE; + } + if ("black".equals(color)) { + return Utils.BLACK; + } + if ("grey".equals(color)) { + return Utils.GREY; + } + if ("white".equals(color)) { + return Utils.WHITE; + } + if ("yellow".equals(color)) { + return Utils.YELLOW; + } + if ("cyan".equals(color)) { + return Utils.CYAN; + } + if ("magenta".equals(color)) { + return Utils.MAGENTA; + } + return Utils.GREY; + } + + public FabrikStructure3D getStructure() { + return structure; + } + + public void openWindow(String name) { + if (windows.containsKey(name)) { + info("%s already started", name); + return; + } + WindowWorker worker = new WindowWorker(this, name); + windows.put(name, worker); + worker.start(); + } + + public void stopService() { + super.stopService(); + for (WindowWorker worker : windows.values()) { + worker.shutdown(); + } + } + + public void test() { + + /* @param directionUV The initial direction of the new bone. + * @param hingeRotationAxis The axis about which the hinge rotates. + * @param hingeReferenceAxis The axis about which any clockwise/anticlockwise rotation constraints are enforced. + */ + + addBaseBone(0, 10, 0, -10, 10, 0); + // addHingeBone("x", 5, "x", 70, 10, "z", "grey"); + addHingeBone("-x", 5, "x", 10, 60, "z", "grey"); + addHingeBone("-y", 20, "z", 90, 90, "-y", "red"); + addFreelyRotatingHingedBone("y", 18, "y", "grey"); + + FabrikChain3D chain = chains.get("default"); + chain.solveForTarget(new Vec3f(-30, -300, -30)); + + for (FabrikBone3D bone : chains.get("default").getChain()) { + bone.getStartLocation().getGlobalPitchDegs(); + bone.getStartLocation().getGlobalYawDegs(); + Vec3f.getDirectionUV(bone.getStartLocation(), bone.getEndLocation()); + + System.out.println("Bone X: " + bone.getStartLocation().toString()); + } + + openWindow("default"); + + log.info("here"); + + // FabrikBone3D base =new FabrikBone3D(new Vec3f(),new + // Vec3f(0.0f,50.0f,0.0f)); + // + // chain.addBone(base); + // FabrikBone3D bone1 = new FabrikBone3D(new Vec3f(0.0f,50.0f,0.0f), new + // Vec3f(0f, 0f, 100f)); + // FabrikBone3D bone2 = new FabrikBone3D(new Vec3f(0f, 0f, 100f), new + // Vec3f(0f, 0f, 200f)); + + // chain.setEff + + // for(intboneLoop =0;boneLoop + // <5;++boneLoop){chain.addConsecutiveBone(newVec2f(0.0f,1.0f),50.0f); + + // Create a FabrikStructure3D + // FabrikStructure3D structure = new FabrikStructure3D(); + // + // // Create bones and joints + // FabrikBone3D root = new FabrikBone3D(0, 0, 0); + // FabrikBone3D bone1 = new FabrikBone3D(0, 0, 100); + // FabrikBone3D bone2 = new FabrikBone3D(0, 0, 100); + // + // // Create joints (optional but can be used for constraints) + // FabrikJoint3D joint1 = new FabrikJoint3D(); + // FabrikJoint3D joint2 = new FabrikJoint3D(); + // + // // Add bones and joints to the structure + // structure.addBone(root); + // structure.addBone(bone1); + // structure.addBone(bone2); + // + // // Set up the chain + // FabrikChain3D chain = new FabrikChain3D("ChainName"); + // chain.addBone(root); + // chain.addBone(bone1); + // chain.addBone(bone2); + // + // // Set the target position for the end effector (bone2) + // chain.setEffectorLocation(0, 0, 300); + // + // // Solve the IK problem + // structure.solveForTarget(chain.getEffectorLocation()); + // + // // Print the bone positions after solving + // for (FabrikBone3D bone : structure.getChain("ChainName").getChain()) { + // System.out.println("Bone X: " + bone.getStartLocation().getX() + + // ", Y: " + bone.getStartLocation().getY() + + // ", Z: " + bone.getStartLocation().getZ()); + // } + // + } + + public FabrikChain3D getChain(String name) { + if (!chains.containsKey(name)) { + error("no chain %s", name); + return null; + } + return chains.get(name); + } + +} diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index a093f61565..e82bdfe744 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -58,6 +58,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(); diff --git a/src/main/java/org/myrobotlab/service/Git.java b/src/main/java/org/myrobotlab/service/Git.java index 1d2d3766f5..8832326052 100644 --- a/src/main/java/org/myrobotlab/service/Git.java +++ b/src/main/java/org/myrobotlab/service/Git.java @@ -3,7 +3,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Properties; @@ -31,6 +30,7 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.submodule.SubmoduleWalk; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; @@ -45,7 +45,7 @@ public class Git extends Service { public final static Logger log = LoggerFactory.getLogger(Git.class); - transient static TextProgressMonitor monitor = new TextProgressMonitor(); + transient ProgressMonitor monitor = new ProgressMonitor(); Map repos = new TreeMap<>(); @@ -56,76 +56,43 @@ public static class RepoData { String branch; String location; String url; - List branches; String checkout; transient org.eclipse.jgit.api.Git git; - public RepoData(String location, String url, List branches, String checkout, org.eclipse.jgit.api.Git git) { + public RepoData(String location, String url, String checkout, org.eclipse.jgit.api.Git git) { this.location = location; this.url = url; - this.branches = branches; this.checkout = checkout; this.git = git; } } + // TODO - overload updates to publish + public class ProgressMonitor extends TextProgressMonitor { + + } + public Git(String n, String id) { super(n, id); } - - // max complexity clone - public void clone(String location, String url, List inbranches, String incheckout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - - File repoLocation = new File(location); - org.eclipse.jgit.api.Git git = null; - Repository repo = null; - - List branches = new ArrayList<>(); - for (String b : inbranches) { - if (!b.contains("refs")) { - branches.add("refs/heads/" + b); - } - } - - String checkout = (incheckout.contains("refs")) ? incheckout : "refs/heads/" + incheckout; - - if (!repoLocation.exists()) { - // clone - log.info("cloning {} {} into {}", url, incheckout, location); - git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranchesToClone(branches).setBranch(checkout).call(); - - } else { - // Open an existing repository - String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; - log.info("opening repo {} from {}", gitDir, url); - repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); - git = new org.eclipse.jgit.api.Git(repo); - } - - repo = git.getRepository(); - - // checkout - log.info("checking out {}", incheckout); - // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" - // + incheckout).call(); - git.branchCreate().setForce(true).setName(incheckout).setStartPoint(incheckout).call(); - git.checkout().setName(incheckout).call(); - - repos.put(location, new RepoData(location, url, inbranches, incheckout, git)); - + + public void clone(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + clone(location, url, branch, checkout, false); } + + // max complexity sync - public void sync(String location, String url, List branches, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + public void sync(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { // initial clone - clone(location, url, branches, checkout); + clone(location, url, branch, checkout); addTask(checkStatusIntervalMs, "checkStatus"); } public void sync(String location, String url, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - sync(location, url, Arrays.asList(checkout), checkout); + sync(location, url, checkout, checkout); } public RevCommit checkStatus() throws WrongRepositoryStateException, InvalidConfigurationException, InvalidRemoteException, CanceledException, RefNotFoundException, @@ -167,7 +134,7 @@ public RevCommit publishPull(RevCommit commit) { return commit; } - private static List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { List ret = new ArrayList<>(); Repository repository = git.getRepository(); @@ -193,17 +160,17 @@ public void stopSync() { purgeTasks(); } - static public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, - RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { + public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, + NoHeadException, TransportException, IOException, GitAPIException { return pull(null, null); } - static public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, + public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { return pull(null, branch); } - static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { + org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -224,7 +191,7 @@ static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { return git; } - static public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { if (src == null) { @@ -278,11 +245,11 @@ static public int pull(String src, String branch) throws IOException, WrongRepos return 0; } - static public void init() throws IllegalStateException, GitAPIException { + public void init() throws IllegalStateException, GitAPIException { init(null); } - static public void init(String directory) throws IllegalStateException, GitAPIException { + public void init(String directory) throws IllegalStateException, GitAPIException { if (directory == null) { directory = System.getProperty("user.dir"); } @@ -291,55 +258,30 @@ static public void init(String directory) throws IllegalStateException, GitAPIEx org.eclipse.jgit.api.Git git = org.eclipse.jgit.api.Git.init().setDirectory(dir).call(); } - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.INFO); - - Properties properties = Platform.gitProperties(); - Git.removeProps(); - log.info("{}", properties); - - /* - * // start the service Git git = (Git) Runtime.start("git", "Git"); - * - * // check out and sync every minute // git.sync("test", - * "https://github.com/MyRobotLab/WorkE.git", "master"); // - * git.sync("/lhome/grperry/github/mrl/myrobotlab", // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - * git.gitPull("agent-removal"); // - * git.sync(System.getProperty("user.dir"), // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - */ - } catch (Exception e) { - log.error("main threw", e); - } - } - - public static String getBranch() throws IOException { + public String getBranch() throws IOException { return getBranch(null); } - public static String getBranch(String src) throws IOException { + public String getBranch(String src) throws IOException { org.eclipse.jgit.api.Git git = getGit(src); return git.getRepository().getBranch(); } - public static Status status() throws NoWorkTreeException, IOException, GitAPIException { + public Status status() throws NoWorkTreeException, IOException, GitAPIException { return status(null); } - public static Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { + public Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { org.eclipse.jgit.api.Git git = getGit(src); Status status = git.status().call(); return status; } - public static void removeProps() { + public void removeProps() { removeProps(null); } - public static void removeProps(String rootFolder) { + public void removeProps(String rootFolder) { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -349,4 +291,89 @@ public static void removeProps(String rootFolder) { } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + Properties properties = Platform.gitProperties(); + // Git.removeProps(); + log.info("{}", properties); + Git git = (Git) Runtime.start("git", "Git"); + git.clone("./depthai", "https://github.com/luxonis/depthai.git", "main", "refs/tags/v1.13.1-sdk", true); + log.info("here"); + + + } catch (Exception e) { + log.error("main threw", e); + } + } + + // max complexity clone and checkout + public void clone(String location, String url, String branch, String checkout, boolean recursive) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + + File repoLocation = new File(location); + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // git clone + + if (!repoLocation.exists()) { + // clone + log.info("cloning {} {} checking out {} into {}", url, branch, checkout, location); + git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranch(branch).call(); + + } else { + // Open an existing repository + String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; + log.info("opening repo {} from {}", gitDir, url); + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + } + + repo = git.getRepository(); + + // git pull + + PullCommand pullCmd = git.pull() + // .setRemote(remoteName) + // .setCredentialsProvider(new + // UsernamePasswordCredentialsProvider(username, password)) + .setRemoteBranchName(branch); + + // Perform the pull operation + pullCmd.call(); + + + // recursive + if (recursive) { + + // Recursively fetch and checkout submodules if they exist + SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repo); + while (submoduleWalk.next()) { + String submodulePath = submoduleWalk.getPath(); + org.eclipse.jgit.api.Git submoduleGit = org.eclipse.jgit.api.Git.open(new File(location, submodulePath)); + submoduleGit.fetch() + .setRemote("origin") + .call(); + submoduleGit.checkout() + .setName(branch) // Replace with the desired branch name + .call(); + } + + } + + if (checkout != null) { + // checkout + log.info("checking out {}", checkout); + // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" + // + incheckout).call(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(checkout).call(); + git.checkout().setName(checkout).call(); + } + + repos.put(location, new RepoData(location, url, checkout, git)); + + } + } diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 51dac5ae44..93ffd39f0f 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -25,10 +25,11 @@ import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; +import org.myrobotlab.service.Log.LogEntry; import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.data.Event; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -54,6 +55,8 @@ public class InMoov2 extends Service implements ServiceLifeCycleL static String speechRecognizer = "WebkitSpeechRecognition"; + protected static final Set stateDefaults = new TreeSet<>(); + /** * This method will load a python file into the python interpreter. * @@ -93,6 +96,23 @@ public static boolean loadFile(String file) { return true; } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.ERROR); + // identical to command line start + // Runtime.startConfig("inmoov2"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + } catch (Exception e) { + log.error("main threw", e); + } + } + + /** + * the config that was processed before booting, if there was one. + */ + String bootedConfig = null; + protected transient ProgramAB chatBot; protected List configList; @@ -103,16 +123,35 @@ public static boolean loadFile(String file) { */ protected boolean configStarted = false; - String currentConfigurationName = "default"; + /** + * map of events or states to sounds + */ + protected Map customSoundMap = new TreeMap<>(); protected transient SpeechRecognizer ear; + protected List errors = new ArrayList<>(); + + /** + * The finite state machine is core to managing state of InMoov2. There is + * very little benefit gained in having the interactions pub/sub. Therefore, + * there will be a direct reference to the fsm. If different state graph is + * needed, then the fsm can provide that service. + */ + private transient FiniteStateMachine fsm = null; // waiting controable threaded gestures we warn user protected boolean gestureAlreadyStarted = false; protected Set gestures = new TreeSet(); + /** + * Prevents actions or events from happening when InMoov2 is first booted + */ + private boolean hasBooted = false; + + protected boolean isPirOn = false; + protected transient HtmlFilter htmlFilter; protected transient ImageDisplay imageDisplay; @@ -121,28 +160,66 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected LedDisplayData led = new LedDisplayData(); + protected Map ledDisplayMap = new TreeMap<>(); /** * supported locales */ protected Map locales = null; - protected int maxInactivityTimeSeconds = 120; - protected transient SpeechSynthesis mouth; protected boolean mute = false; protected transient OpenCV opencv; + protected List peersStarted = new ArrayList<>(); + protected transient Python python; + protected long stateLastIdleTime = System.currentTimeMillis(); + + protected long stateLastRandomTime = System.currentTimeMillis(); + protected String voiceSelected; + protected boolean wasMutedBeforeBoot = false; public InMoov2(String n, String id) { super(n, id); + + // add the default InMoov2 state handlers - so the FSM can invoke them + // this is hardcode, because it requires Java methods in InMoov2 + // so it makes sense to hardcode them... + // if a user needs something different, it will happen in pyton-land + // consequence it this will need maintenance if there are new InMoov2 java + // state handlers + stateDefaults.add("wake"); + stateDefaults.add("firstInit"); + stateDefaults.add("idle"); + stateDefaults.add("random"); + stateDefaults.add("sleep"); // listens & dreams, no opencv, waits for + // wakeWord, pir active + stateDefaults.add("powerDown"); // stops heartbeat, listening ? + stateDefaults.add("shutdown");// ends mrl + + ledDisplayMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); + ledDisplayMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); + ledDisplayMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); + ledDisplayMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); + ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); + ledDisplayMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); + ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); + + customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); + customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); + customSoundMap.put("idle", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/start.wav")); + customSoundMap.put("random", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/reveal.wav")); + customSoundMap.put("sleep", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/back.wav")); + customSoundMap.put("powerDown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("shutdown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/marimba.wav")); + } public void addTextListener(TextListener service) { @@ -451,9 +528,46 @@ public void finishedGesture(String nameOfGesture) { } } - // FIXME - this isn't the callback for fsm - why is it needed here ? - public void fire(String event) { - invoke("publishEvent", event); + public void firstInit() { + log.info("firstInit"); + // cheap way to prevent race condition + // of "wake" firing a state change .. which will spawn + // a system event of FIRST_INIT that will answer this + // question ... + sleep(2000); + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + if (chatBot != null) { + chatBot.getResponse("FIRST_INIT"); + } + } + + public void flash(String name) { + LedDisplayData led = ledDisplayMap.get(name); + if (led == null) { + led = ledDisplayMap.get("default"); + } + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + + /** + * used to configure a flashing event - could use configuration to signal + * different colors and states + * + * @return + */ + public void flash() { + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + } + + public void flash(int r, int g, int b, int count) { + // FIXME - this should be checking a protected "state" + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); + } } public void fullSpeed() { @@ -572,6 +686,10 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } + public String getState() { + return fsm.getCurrent(); + } + /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -616,12 +734,10 @@ public void halfSpeed() { } /** - * execute python scripts in the init directory on startup of the service - * - * @throws IOException + * pir active ear listening for wakeword */ - public void loadInitScripts() throws IOException { - loadScripts(getResourceDir() + fs + "init"); + public void idle() { + log.info("idle"); } public boolean isCameraOn() { @@ -815,6 +931,115 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "moveToBlocking", topStom, midStom, lowStom); } + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + synchronized public void onBoot() { + + // thinking you shouldn't "boot" twice ? + if (hasBooted) { + log.warn("will not boot again"); + return; + } + + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) + try { + // copy config if it doesn't already exist + String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + List files = FileIO.getFileList(resourceBotDir); + for (File f : files) { + String botDir = "data/config/" + f.getName(); + File bDir = new File(botDir); + if (bDir.exists() || !f.isDirectory()) { + log.info("skipping data/config/{}", botDir); + } else { + log.info("will copy new data/config/{}", botDir); + try { + FileIO.copy(f.getAbsolutePath(), botDir); + } catch (Exception e) { + error(e); + } + } + } + } catch (Exception e) { + error(e); + } + + // FIXME - find good way of running an animation "through" a state + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + invoke("publishPlayAnimation", config.bootAnimation); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG STARTED %s", bootedConfig); + } + + for (String peerKey : peersStarted) { + systemEvent("STARTED %s", peerKey); + } + + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + + fsm.fire("wake"); + + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + + hasBooted = true; + } + public PredicateEvent onChangePredicate(PredicateEvent event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { @@ -843,12 +1068,6 @@ public void onCreated(String fullname) { log.info("{} created", fullname); } - public void onFinishedConfig(String configName) { - log.info("onFinishedConfig"); - // invoke("publishEvent", "configFinished"); - invoke("publishFinishedConfig", configName); - } - public void onGestureStatus(Status status) { if (!status.equals(Status.success()) && !status.equals(Status.warn("Python process killed !"))) { error("I cannot execute %s, please check logs", lastGestureExecuted); @@ -858,6 +1077,87 @@ public void onGestureStatus(Status status) { unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + /** + * A generalized recurring event which can preform checks and various other + * methods or tasks. Heartbeats will not start until after boot stage. + */ + public void onHeartbeat() { + try { + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + + if (!hasBooted) { + log.info("boot hasn't completed, will not process heartbeat"); + return; + } + + Long lastActivityTime = getLastActivityTime(); + + // FIXME lastActivityTime != 0 is bogus - the value should be null if + // never set + if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0 + && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } + + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fsm.fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } + + // interval event firing + if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + // fsm.fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } + + } catch (Exception e) { + error(e); + } + + if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) { + flash("pirOn"); + } + + if (config.batteryLevelCheck) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } + } + + // flash error until errors are cleared + if (config.healthCheckFlash) { + if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { + invoke("publishFlash", ledDisplayMap.get("error")); + } else if (ledDisplayMap.containsKey("heartbeat")) { + LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); + invoke("publishFlash", heartbeat); + } + } + + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + + } + + /** + * Central hub of input motion control. Potentially, all input from + * joysticks, quest2 controllers and headset, or any IK service could + * be sent here + */ @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -878,21 +1178,66 @@ public void onJoystickInput(JoystickData input) throws Exception { invoke("publishEvent", "joystick"); } - public String onNewState(String state) { - log.error("onNewState {}", state); + /** + * Centralized logging system will have all logging from all services, + * including lower level logs that do not propegate as statuses + * + * @param log + * - flushed log from Log service + */ + public void onLogEvents(List log) { + // scan for warn or errors + for (LogEntry entry : log) { + if ("ERROR".equals(entry.level) && errors.size() < 100) { + errors.add(entry); + } + } + } - // put configurable filter here ! + public void onMoveHead(Map map) { + InMoov2Head head = (InMoov2Head) getPeer("head"); + if (head != null) { + head.onMove(map); + } + } - // state substitutions ? - // let python subscribe directly to fsm.publishNewState + public void onMoveLeftArm(Map map) { + InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); + if (leftArm != null) { + leftArm.onMove(map); + } + } - // if - invoke(state); - // depending on configuration .... - // call python ? - // fire fsm events ? - // do defaults ? - return state; + public void onMoveLeftHand(Map map) { + InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); + if (leftHand != null) { + leftHand.onMove(map); + } + } + + // public Message publishPython(String method, Object...data) { + // return Message.createMessage(getName(), getName(), method, data); + // } + + public void onMoveRightArm(Map map) { + InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); + if (rightArm != null) { + rightArm.onMove(map); + } + } + + public void onMoveRightHand(Map map) { + InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); + if (rightHand != null) { + rightHand.onMove(map); + } + } + + public void onMoveTorso(Map map) { + InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); + if (torso != null) { + torso.onMove(map); + } } public OpenCVData onOpenCVData(OpenCVData data) { @@ -901,35 +1246,35 @@ public OpenCVData onOpenCVData(OpenCVData data) { } /** - * initial callback for Pir sensor Default behavior will be: send fsm event - * onPirOn flash neopixel + * onPeak volume callback TODO - maybe make it variable with volume ? + * + * @param volume + */ + public void onPeak(double volume) { + if (config.neoPixelFlashWhenSpeaking && !configStarted) { + if (volume > 0.5) { + if (ledDisplayMap.get("onPeakColor") != null) { + LedDisplayData onPeakColor = ledDisplayMap.get("onPeakColor"); + invoke("publishFlash", onPeakColor); + } + } + } + } + + /** + * Pir on callback */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.interval = 500; - // FIXME flash on config.flashOnBoot - invoke("publishFlash"); - // pirOn event vs wake event - invoke("publishEvent", "WAKE"); + isPirOn = true; + fsm.fire("wake"); } - // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python - // yet provides a stable default, which can be fully replaced - // Works using common pub/sub rules - // TODO - this is a loopback power up - // its replaceable by typical subscription rules - public void onPowerUp() { - // CON - type aware - NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); - // CON - necessary NPE checking - if (neoPixel != null) { - neoPixel.setColor(0, 130, 0); - neoPixel.playAnimation("Larson Scanner"); - } + /** + * Pir off callback + */ + public void onPirOff() { + isPirOn = false; + fsm.fire("sleep"); } @Override @@ -1101,6 +1446,81 @@ public void onStarted(String name) { } } + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param stateChange + * @return + */ + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { + try { + log.error("onStateChange {}", stateChange); + + String current = stateChange.current; + String last = stateChange.last; + + // leaving random state + if ("random".equals(last) && !"random".equals(current) && isPeerStarted("random")) { + Random random = (Random) getPeer("random"); + random.disable(); + } + + if ("wake".equals(last)) { + invoke("publishStopAnimation"); + } + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", current); + } + + if (config.customSounds && customSoundMap.containsKey(current)) { + invoke("publishPlayAudioFile", customSoundMap.get(current)); + } + + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(current)) { + invoke(current); + } + + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library + + // put configurable filter here ! + + // state substitutions ? + // let python subscribe directly to fsm.publishStateChange + + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library + + // if + // invoke(state); + // depending on configuration .... + // call python ? + // fire fsm events ? + // do defaults ? + } catch (Exception e) { + error(e); + } + return stateChange; + } + @Override public void onStopped(String name) { // using release peer for peer releasing @@ -1117,36 +1537,25 @@ public void onText(String text) { invoke("publishText", text); } - // TODO FIX/CHECK this, migrate from python land public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) rest(); - purgeTasks(); + purgeTasks(); // including heartbeat disable(); - if (ear != null) { - ear.lockOutAllGrammarExcept("power up"); + if (chatBot != null) { + chatBot.sleep(); } - // FIXME - DO NOT DO THIS !!!! SIMPLY PUBLISH A POWER DOWN EVENT AND PYTHON - // CAN SUBSCRIBE - // AND MAINTAIN A SET OF onPowerDown: callback methods - python.execMethod("power_down"); - } - - // TODO FIX/CHECK this, migrate from python land - // FIXME - defaultPowerUp switchable + override - public void powerUp() { - enable(); - rest(); - if (ear != null) { - ear.clearLock(); + // FIXME - bad remove it - what is needed ? + // i think this is legacy wake word + ear.lockOutAllGrammarExcept("power up"); } - beginCheckingOnInactivity(); - - python.execMethod("power_up"); } /** @@ -1167,10 +1576,11 @@ public String publishStartConfig(String configName) { return configName; } - public String publishFinishedConfig(String configName) { - info("config %s finished", configName); - invoke("publishEvent", "CONFIG LOADED " + configName); + public void publishBoot() { + log.info("publishBoot"); + } + public String publishConfigFinished(String configName) { return configName; } @@ -1184,15 +1594,19 @@ public List publishConfigList() { return configList; } - /** - * event publisher for the fsm - although other services potentially can - * consume and filter this event channel - * - * @param event - * @return - */ - public String publishEvent(String event) { - return String.format("SYSTEM_EVENT %s", event); + public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + return data; + } + + public LedDisplayData publishFlash(LedDisplayData data) { + return data; } /** @@ -1217,8 +1631,17 @@ public String publishHeartbeat() { } /** - * A more extensible interface point than publishEvent - * FIXME - create interface for this + * if inactivityTime configured, this event is published after there has not + * been in activity since. + */ + public void publishInactivity() { + log.info("publishInactivity"); + fsm.fire("inactvity"); + } + + /** + * A more extensible interface point than publishEvent FIXME - create + * interface for this * * @param msg * @return @@ -1298,23 +1721,36 @@ public HashMap publishMoveRightArm(Double bicep, Double rotate, return map; } - public HashMap publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - return map; + public String publishPlayAudioFile(String filename) { + return filename; } - public HashMap publishMoveTorso(Double topStom, Double midStom, Double lowStom) { - HashMap map = new HashMap<>(); - map.put("topStom", topStom); - map.put("midStom", midStom); - map.put("lowStom", lowStom); - return map; + public String publishPlayAnimation(String animation) { + return animation; + } + + /** + * stop animation event + */ + public void publishStopAnimation() { + } + + public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { + log.info("publishStateChange {}", state); + return state; + } + + /** + * event publisher for the fsm - although other services potentially can + * consume and filter this event channel + * + * @param event + * @return + */ + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh + return String.format("SYSTEM_EVENT %s", event); } /** @@ -1325,6 +1761,16 @@ public String publishText(String text) { return text; } + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + @Override public void releasePeer(String peerKey) { super.releasePeer(peerKey); @@ -1532,6 +1978,11 @@ public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist); } + // ----------------------------------------------------------------------------- + // These are methods added that were in InMoov1 that we no longer had in + // InMoov2. + // From original InMoov1 so we don't loose the + public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); } @@ -1562,10 +2013,17 @@ public void setVoice(String name) { } } - // ----------------------------------------------------------------------------- - // These are methods added that were in InMoov1 that we no longer had in - // InMoov2. - // From original InMoov1 so we don't loose the + public void shutdown() { + log.info("shutdown"); + Runtime.shutdown(); + } + + /** + * ear still listening pir still active + */ + public void sleep() { + log.info("sleep"); + } public void sleeping() { log.error("sleeping"); @@ -1790,53 +2248,33 @@ public void startService() { // get service start and release life cycle events runtime.attachServiceLifeCycleListener(getName()); - - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); - } - } - - // get events of new services and shutdown - subscribe("runtime", "shutdown"); - // power up loopback subscription - addListener(getName(), "powerUp"); - - subscribe("runtime", "publishConfigList"); - if (runtime.isProcessingConfig()) { - invoke("publishEvent", "configStarted"); - } - subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); - // chatbot getresponse attached to publishEvent - addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + // subscribe to config processing events + // runtime callbacks publish the same a local + subscribe("runtime", "publishConfigStarted", "publishConfigStarted"); + subscribe("runtime", "publishConfigFinished", "publishConfigFinished"); - try { - // copy config if it doesn't already exist - String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); - List files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/config/" + f.getName(); - File bDir = new File(botDir); - if (bDir.exists() || !f.isDirectory()) { - log.info("skipping data/config/{}", botDir); - } else { - log.info("will copy new data/config/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } + runtime.invoke("publishConfigList"); + + // iterate through existing started service + // add them to peers booted + for (String name : Runtime.getServiceNames()) { + String peerKey = getPeerKey(name); + if (peerKey != null) { + peersStarted.add(peerKey); } - } catch (Exception e) { - error(e); } - runtime.invoke("publishConfigList"); + if (runtime.isProcessingConfig()) { + // if InMoov2 was started as part of a config set + // set here so boot can be delayed until the config + // set is done + configStarted = true; + bootedConfig = runtime.getConfigName(); + } else { + invoke("publishBoot"); + } } public void startServos() { @@ -1870,6 +2308,7 @@ public void stopGesture() { public void stopHeartbeat() { purgeTask("publishHeartbeat"); + config.heartbeat = false; } public void stopNeopixelAnimation() { @@ -1896,7 +2335,17 @@ public void systemCheck() { Platform platform = Runtime.getPlatform(); setPredicate("system version", platform.getVersion()); // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? + } + + public String systemEvent(String eventMsg) { + invoke("publishSystemEvent", eventMsg); + return eventMsg; + } + + public String systemEvent(String format, Object... ags) { + String eventMsg = String.format(format, ags); + return systemEvent(eventMsg); } // FIXME - if this is really desired it will drive local references for all @@ -1911,89 +2360,156 @@ public void waitTargetPos() { sendToPeer("torso", "waitTargetPos"); } + public void closeRightHand() { - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.ERROR); - // Platform.setVirtual(true); - // Runtime.start("s01", "Servo"); - // Runtime.start("intro", "Intro"); - - // Runtime.startConfig("pr-1213-1"); - - Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); - - boolean done = true; - if (done) { - return; - } - + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - // webgui.setSsl(true); - webgui.autoStartBrowser(false); - // webgui.setPort(8888); - webgui.startService(); + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveRightHand", map); - Runtime.start("python", "Python"); - // Runtime.start("ros", "Ros"); - Runtime.start("intro", "Intro"); - // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - // i01.startPeer("simulator"); - // Runtime.startConfig("i01-05"); - // Runtime.startConfig("pir-01"); + } - // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); - // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); + public void openRightHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveRightHand", map); + } + + public void closeLeftHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveLeftHand", map); - // polly.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - // i01.startPeer("mouth"); - // i01.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); + } - Runtime.start("python", "Python"); + public void openLeftHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - // i01.startSimulator(); - Plan plan = Runtime.load("webgui", "WebGui"); - // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui"); - // webgui.autoStartBrowser = false; - Runtime.startConfig("webgui"); - Runtime.start("webgui", "WebGui"); + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveLeftHand", map); + } - Random random = (Random) Runtime.start("random", "Random"); + public void openHands() { + openLeftHand(); + openRightHand(); + } - random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + public void closeHands() { + closeLeftHand(); + closeRightHand(); + } + + public Event onEvent(Event event) { + + return event; + } + + public void wake() { + log.info("wake"); + // do waking things - based on config + + // blink + + // wake gesture + // callback + // imoov2[{name}]["onWake"](this) + /** + *

    +     i01.speakBlocking("I was sleeping")
    +     lookrightside()
    +     sleep(2)
    +     lookleftside()
    +     sleep(4)
    +     relax()
    +     ear.clearLock()
    +     sleep(2)
    +     i01.finishedGesture()
    +     * 
    + */ + + /** + *
    +     * // legacy
    +     * enable();
    +     * rest();
    +     * 
    +     * if (ear != null) {
    +     *   ear.clearLock();
    +     * }
    +     * 
    +     * // beginCheckingOnInactivity();
    +     * // BAD BAD BAD !!!
    +     * publishEvent("powerUp"); // before or after loopback
    +     * 
    + **/ + // was a relax gesture .. might want to ask about it .. + + // if ear start listening + AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) getPeer("ear"); + if (ear != null) { + ear.startListening(); + } - random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + // attempt recognize where its at - random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + // attempt to recognize people - random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); - random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); + // look for activity - random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); - random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); + // say hello - random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); - random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); + // start animation (configurable) - random.save(); + rest(); -// i01.startChatBot(); -// -// i01.startAll("COM3", "COM4"); - Runtime.start("python", "Python"); + // should "session be determined by recognition?" + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); - } catch (Exception e) { - log.error("main threw", e); + if (chatBot != null) { + String firstinit = chatBot.getPredicate("firstinit"); + // wtf - "ok" really, for a boolean? + if (!"ok".equals(firstinit)) { + fsm.fire("firstInit"); + } } } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index 11d71fba7c..a8b3fb2b4f 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -142,15 +142,34 @@ public void disable() { eyelidRight.disable(); } - public long getLastActivityTime() { - - long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + public Long getLastActivityTime() { + + Long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); + if (getPeer("eyeX") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); + } + if (getPeer("eyeY") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); + } + if (getPeer("jaw") != null) { + lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rothead.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, neck.getLastActivityTime()); + } + + if (getPeer("eyelidLeft") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); + } + if (getPeer("eyelidRight") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + } return lastActivityTime; } diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 05524a2630..378fd2440c 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -247,6 +247,12 @@ public void publishTelemetry(String name) { log.info("Servo : {} Angle : {}", jointName, angleMap.get(jointName)); } invoke("publishJointAngles", angleMap); + + InMoov2 i01 = (InMoov2)Runtime.getService("i01"); + if (i01 != null) { + i01.onJointAngles(angleMap); + } + // we want to publish the joint positions // this way we can render on the web gui.. double[][] jointPositionMap = createJointPositionMap(name); @@ -292,8 +298,7 @@ public static void main(String[] args) throws Exception { LoggingFactory.init("info"); String arm = "myArm"; - Runtime.createAndStart("python", "Python"); - Runtime.createAndStart("gui", "SwingGui"); + Runtime.start("python", "Python"); InverseKinematics3D inversekinematics = (InverseKinematics3D) Runtime.start("ik3d", "InverseKinematics3D"); // InverseKinematics3D inversekinematics = new InverseKinematics3D("iksvc"); diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index fbc5ef5e99..2174ac739b 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -1469,7 +1469,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": @@ -2157,7 +2157,7 @@ public void simpleInitApp() { new File(getDataDir()).mkdirs(); new File(getResourceDir()).mkdirs(); - // assetManager.registerLocator("./", FileLocator.class); + assetManager.registerLocator("./", FileLocator.class); assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLocator(assetsDir, FileLocator.class); assetManager.registerLocator(modelsDir, FileLocator.class); diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java index fa88732014..3e1fe48847 100644 --- a/src/main/java/org/myrobotlab/service/Log.java +++ b/src/main/java/org/myrobotlab/service/Log.java @@ -34,7 +34,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.config.LogConfig; import org.slf4j.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -44,7 +44,7 @@ import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.status.Status; -public class Log extends Service implements Appender { +public class Log extends Service implements Appender { public static class LogEntry { public long ts; @@ -81,7 +81,7 @@ public String toString() { * broadcast logging is through publishLogEvent (not broadcastState) */ transient List buffer = new ArrayList<>(); - + /** * logging state */ @@ -192,6 +192,7 @@ public void doAppend(ILoggingEvent event) throws LogbackException { synchronized public void flush() { if (buffer.size() > 0) { invoke("publishLogEvents", buffer); + buffer = new ArrayList<>(maxSize); lastPublishLogTimeTs = System.currentTimeMillis(); } @@ -224,6 +225,11 @@ public List publishLogEvents(List entries) { return entries; } + public List publishErrors(List entries) { + return entries; + } + + @Override public void setContext(Context arg0) { // TODO Auto-generated method stub diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e13daf273d..924f7ef219 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -60,6 +60,12 @@ private class AnimationRunner implements Runnable { public void run() { try { running = true; + while (running) { + LedDisplayData led = displayQueue.take(); + // save existing state if necessary .. + // stop animations if running + // String lastAnimation = currentAnimation; + if ((led.count > 0) && (currentAnimation == null)){ while (running) { equalizer(); @@ -430,6 +436,33 @@ public void flash(int count, long interval, int r, int g, int b) { } + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, flashTimeOn, flashTimeOff); + } + + public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + displayQueue.add(data); + } + + public void onPlayAnimation(String animation) { + playAnimation(animation); + } + + public void onStopAnimation() { + stopAnimation(); + } + + public void onFlash(LedDisplayData data) { + displayQueue.add(data); + } + public void flashBrightness(double brightNess) { NeoPixelConfig c = (NeoPixelConfig)config; @@ -580,6 +613,23 @@ public int getRed() { @Override public void playAnimation(String animation) { + if (animation == null) { + log.info("playAnimation null"); + return; + } + + if (animation.equals(currentAnimation)) { + log.info("already playing {}", currentAnimation); + return; + } + +// if ("Snake".equals(animation)){ +// LedDisplayData snake = new LedDisplayData(); +// snake.red = red; +// snake.green = green; +// snake.blue = blue; +// displayQueue.add(null); +// } else if (animations.containsKey(animation)) { currentAnimation = animation; 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/Pir.java b/src/main/java/org/myrobotlab/service/Pir.java index 5a2f0d750c..eea710f585 100644 --- a/src/main/java/org/myrobotlab/service/Pir.java +++ b/src/main/java/org/myrobotlab/service/Pir.java @@ -9,7 +9,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.PirConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.PinArrayControl; import org.myrobotlab.service.interfaces.PinDefinition; @@ -52,33 +51,31 @@ public void attach(String name) { } public void setPinArrayControl(String control) { - PirConfig c = (PirConfig) config; - c.controller = control; + config.controller = control; } public void attachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { error("controller cannot be null"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin should be set before attaching"); return; } - c.controller = CodecUtils.getShortName(control); + config.controller = CodecUtils.getShortName(control); // fire and forget - send(c.controller, "attach", getName()); + send(config.controller, "attach", getName()); // assume worky isAttached = true; // enable if configured - if (c.enable) { - send(c.controller, "enablePin", c.pin, c.rate); + if (config.enable) { + send(config.controller, "enablePin", config.pin, config.rate); } broadcastState(); @@ -95,16 +92,15 @@ public void detach(String name) { * @param control */ public void detachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { log.info("detaching null"); return; } - if (c.controller != null) { - if (!c.controller.equals(control)) { - log.warn("attempting to detach {} but this pir is attached to {}", control, c.controller); + if (config.controller != null) { + if (!config.controller.equals(control)) { + log.warn("attempting to detach {} but this pir is attached to {}", control, config.controller); return; } } @@ -112,8 +108,8 @@ public void detachPinArrayControl(String control) { // disable disable(); - send(c.controller, "detach", getName()); - // c.controller = null; left as configuration .. "last controller" + send(config.controller, "detach", getName()); + // config.controller = null; left as configuration .. "last controller" // detached isAttached = false; @@ -128,13 +124,12 @@ public void detachPinArrayControl(String control) { * */ public void disable() { - PirConfig c = (PirConfig) config; - if (c.controller != null && c.pin != null) { - send(c.controller, "disablePin", c.pin); + if (config.controller != null && config.pin != null) { + send(config.controller, "disablePin", config.pin); } - c.enable = false; + config.enable = false; active = null; broadcastState(); } @@ -143,8 +138,7 @@ public void disable() { * Enables polling at the preset poll rate. */ public void enable() { - PirConfig c = (PirConfig) config; - enable(c.rate); + enable(config.rate); } /** @@ -154,14 +148,13 @@ public void enable() { * */ public void enable(int rateHz) { - PirConfig c = (PirConfig) config; - if (c.controller == null) { + if (config.controller == null) { error("pin control not set"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin not set"); return; } @@ -171,17 +164,16 @@ public void enable(int rateHz) { return; } - c.rate = rateHz; + config.rate = rateHz; /* PinArrayControl.enablePin */ - send(c.controller, "enablePin", c.pin, rateHz); - c.enable = true; + send(config.controller, "enablePin", config.pin, rateHz); + config.enable = true; broadcastState(); } @Override public String getPin() { - PirConfig c = (PirConfig) config; - return c.pin; + return config.pin; } /** @@ -190,8 +182,7 @@ public String getPin() { * @return Hz */ public int getRate() { - PirConfig c = (PirConfig) config; - return c.rate; + return config.rate; } /** @@ -211,8 +202,7 @@ public boolean isActive() { * @return true = Enabled. false = Disabled. */ public boolean isEnabled() { - PirConfig c = (PirConfig) config; - return c.enable; + return config.enable; } @Override @@ -247,13 +237,12 @@ public PirConfig getConfig() { @Override public void onPin(PinData pindata) { - PirConfig c = (PirConfig) config; log.debug("onPin {}", pindata); boolean sense = (pindata.value != 0); // sparse publishing only on state change - if (active == null || active != sense && c.enable) { + if (active == null || active != sense && config.enable) { // state change invoke("publishSense", sense); active = sense; @@ -286,14 +275,12 @@ public void publishPirOff() { */ @Override public void setPin(String pin) { - PirConfig c = (PirConfig) config; - c.pin = pin; + config.pin = pin; } @Deprecated /* use attach(String) */ public void setPinArrayControl(PinArrayControl pinControl) { - PirConfig c = (PirConfig) config; - c.controller = pinControl.getName(); + config.controller = pinControl.getName(); } /** @@ -306,8 +293,7 @@ public void setRate(int rateHz) { error("invalid poll rate - default is 1 Hz valid value is > 0"); return; } - PirConfig c = (PirConfig) config; - c.rate = rateHz; + config.rate = rateHz; } /** diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index 93b8178fd2..dca027aa3a 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -183,14 +183,17 @@ public void addRandom(long minIntervalMs, long maxIntervalMs, String name, Strin msg.interval = getRandom(minIntervalMs, maxIntervalMs); log.info("add random message {} in {} ms", key, msg.interval); - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } broadcastState(); } public void process(String key) { - if (!enabled) { - return; - } + // if (!enabled) { + // return; + // } RandomMessage msg = randomData.get(key); if (msg == null || !msg.enabled) { @@ -230,7 +233,11 @@ public void process(String key) { purgeTask(key); if (!msg.oneShot) { msg.interval = getRandom(msg.minIntervalMs, msg.maxIntervalMs); - addTask(key, 0, msg.interval, "process", key); + // must re-schedule unless one shot + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } } } @@ -316,7 +323,10 @@ public void enable(String key) { return; } randomData.get(key).enabled = true; - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } return; } // must be name - disable "all" for this service @@ -325,7 +335,10 @@ public void enable(String key) { if (msg.name.equals(name)) { msg.enabled = true; String fullKey = String.format("%s.%s", msg.name, msg.method); - addTask(fullKey, 0, msg.interval, "process", fullKey); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(fullKey, 0, msg.interval, "process", fullKey); + } } } } @@ -335,6 +348,7 @@ public void disable() { // events purgeTasks(); enabled = false; + broadcastState(); } public void enable() { @@ -345,7 +359,9 @@ public void enable() { addTask(fullKey, 0, msg.interval, "process", fullKey); } } + enabled = true; + broadcastState(); } public void purge() { @@ -392,9 +408,10 @@ public static void main(String[] args) { List ret = random.getServiceList(); Set mi = random.getMethodsFromName("c1"); List mes = MethodCache.getInstance().query("Clock", "setInterval"); - + random.disable(); random.addRandom(200, 1000, "i01", "setHeadSpeed", 8, 20, 8, 20, 8, 20); random.addRandom(200, 1000, "i01", "moveHead", 65, 115, 65, 115, 65, 115); + random.enable(); // Python python = (Python) Runtime.start("python", "Python"); diff --git a/src/main/java/org/myrobotlab/service/Updater.java b/src/main/java/org/myrobotlab/service/Updater.java index 779dc1c81f..f97f3149e6 100644 --- a/src/main/java/org/myrobotlab/service/Updater.java +++ b/src/main/java/org/myrobotlab/service/Updater.java @@ -4,11 +4,32 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Properties; import java.util.Set; import java.util.TreeSet; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.DetachedHeadException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.BranchTrackingStatus; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.CmdOptions; import org.myrobotlab.framework.MrlException; @@ -392,10 +413,13 @@ public void checkForUpdates() { if (isSrcMode) { String cwd = System.getProperty("user.dir"); boolean makeBuild = false; - String branch = Git.getBranch(); + + Repository repo = new FileRepositoryBuilder().setGitDir(new File(System.getProperty("user.dir"))).build(); + org.eclipse.jgit.api.Git git = new org.eclipse.jgit.api.Git(repo); + String branch = git.getRepository().getBranch(); log.info("current source branch is \"{}\"", branch); - int commitsBehind = Git.pull(branch); + int commitsBehind = pull(null, branch); if (gitProps == null) { log.info("target/classes/git.properties does not exist - will build"); @@ -413,7 +437,8 @@ public void checkForUpdates() { // FIXME - download mvn if it does not exist ?? // remove git properties before compile - Git.removeProps(); + File props = new File(System.getProperty("user.dir") + File.separator + "target" + File.separator + "classes" + File.separator + "git.properties"); + props.delete(); // FIXME - compile or package mode ! String ret = Maven.mvn(cwd, branch, "compile", System.currentTimeMillis() / 1000, offline); @@ -613,6 +638,73 @@ public boolean accept(File dir, String name) { return false; } + TextProgressMonitor monitor = new TextProgressMonitor(); + + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { + + if (src == null) { + src = System.getProperty("user.dir"); + } + + if (branch == null) { + log.warn("branch is not set - setting to default develop"); + branch = "develop"; + } + + List branches = new ArrayList(); + branches.add("refs/heads/" + branch); + + File repoParentFolder = new File(src); + + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // Open an existing repository FIXME Try Git.open(dir) + String gitDir = repoParentFolder.getAbsolutePath() + "/.git"; + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + + repo = git.getRepository(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(branch).call(); + git.checkout().setName(branch).call(); + + git.fetch().setProgressMonitor(monitor).call(); + + List localLogs = getLogs(git, "origin/" + branch, 1); + List remoteLogs = getLogs(git, "remotes/origin/" + branch, 1); + + RevCommit localCommit = localLogs.get(0); + RevCommit remoteCommit = remoteLogs.get(0); + + BranchTrackingStatus status = BranchTrackingStatus.of(repo, branch); + + // FIXME - Git.close() file handles + + if (status.getBehindCount() > 0) { + log.info("local ts {}, remote {} - {} pulling", localCommit.getCommitTime(), remoteCommit.getCommitTime(), remoteCommit.getFullMessage()); + PullCommand pullCmd = git.pull(); + pullCmd.setProgressMonitor(monitor); + pullCmd.call(); + git.close(); + return status.getBehindCount(); + } + log.info("no new commits on branch {}", branch); + git.close(); + return 0; + } + + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { + List ret = new ArrayList<>(); + Repository repository = git.getRepository(); + Iterable logs = git.log().setMaxCount(maxCount).add(repository.resolve(ref)).call(); + for (RevCommit rev : logs) { + ret.add(rev); + } + return ret; + } + public static void main(String[] args) { LoggingFactory.init(Level.INFO); diff --git a/src/main/java/org/myrobotlab/service/config/CalikoConfig.java b/src/main/java/org/myrobotlab/service/config/CalikoConfig.java new file mode 100644 index 0000000000..d1c3ff9554 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/CalikoConfig.java @@ -0,0 +1,36 @@ +package org.myrobotlab.service.config; + +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.service.Pid.PidData; + +public class CalikoConfig extends ServiceConfig { + + public Map data = new HashMap<>(); + + public boolean use3dDemo; + + public int demoNumber; + + public boolean drawConstraints; + + public Object rotateBasesMode; + + public boolean drawLines; + + public boolean drawModels; + + public boolean fixedBaseMode; + + public boolean drawAxes; + + public boolean paused; + + public boolean leftMouseButtonDown; + + public int windowWidth; + + public int windowHeight; + +} diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index ecb2d59c4e..8e07cbf5ee 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -56,7 +56,9 @@ public class InMoov2Config extends ServiceConfig { public boolean openCVFlipPicture=false; public boolean pirEnableTracking = false; - + + public boolean pirOnFlash = true; + /** * play pir sounds when pir switching states * sound located in data/InMoov2/sounds/pir-activated.mp3 @@ -65,10 +67,9 @@ public class InMoov2Config extends ServiceConfig { public boolean pirPlaySounds = true; public boolean pirWakeUp = true; - - public boolean robotCanMoveHeadWhileSpeaking = true; - + public boolean robotCanMoveHeadWhileSpeaking = true; + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest @@ -81,13 +82,45 @@ public class InMoov2Config extends ServiceConfig { public int sleepTimeoutMs=300000; public boolean startupSound = true; - - public int trackingTimeoutMs=10000; + /** + * + */ + public boolean stateChangeIsMute = true; + + /** + * Interval in seconds for a idle state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to idle. Heartbeat will fire the event. + */ + public Integer stateIdleInterval = 120; + + + /** + * Interval in seconds for a random state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to random. Heartbeat will fire the event. + */ + public Integer stateRandomInterval = 120; + + /** + * Determines if InMoov2 publish system events during boot state + */ + public boolean systemEventsOnBoot = false; + + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; + + public int trackingTimeoutMs = 10000; + public String unlockInsult = "forgive me"; public boolean virtual = false; + public String bootAnimation = "Theater Chase"; + public InMoov2Config() { } @@ -112,6 +145,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "left", "Arduino", false); addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false); addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false); + addDefaultPeerConfig(plan, name, "log", "Log", false); addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false); addDefaultPeerConfig(plan, name, "mouthControl", "MouthControl", false); addDefaultPeerConfig(plan, name, "neoPixel", "NeoPixel", false); @@ -256,22 +290,28 @@ public Plan getDefault(Plan plan, String name) { FiniteStateMachineConfig fsm = (FiniteStateMachineConfig) plan.get(getPeerName("fsm")); // TODO - events easily gotten from InMoov data ?? auto callbacks in python if exists ? fsm.current = "boot"; - fsm.transitions.add(new Transition("boot", "configStarted", "applyingConfig")); - fsm.transitions.add(new Transition("applyingConfig", "getUserInfo", "getUserInfo")); - fsm.transitions.add(new Transition("applyingConfig", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("applyingConfig", "wake", "awake")); - fsm.transitions.add(new Transition("getUserInfo", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); - fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); + fsm.transitions.add(new Transition("boot", "wake", "wake")); + fsm.transitions.add(new Transition("wake", "idle", "idle")); + fsm.transitions.add(new Transition("firstInit", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "random", "random")); + fsm.transitions.add(new Transition("random", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "sleep", "sleep")); + fsm.transitions.add(new Transition("sleep", "wake", "wake")); + fsm.transitions.add(new Transition("idle", "powerDown", "powerDown")); + fsm.transitions.add(new Transition("wake", "firstInit", "firstInit")); + // powerDown to shutdown +// fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); +// fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); PirConfig pir = (PirConfig) plan.get(getPeerName("pir")); - pir.pin = "23"; + pir.pin = "D23"; pir.controller = name + ".left"; pir.listeners = new ArrayList<>(); - pir.listeners.add(new Listener("publishPirOn", name, "onPirOn")); - + pir.listeners.add(new Listener("publishPirOn", name)); + pir.listeners.add(new Listener("publishPirOff", name)); + // == Peer - random ============================= RandomConfig random = (RandomConfig) plan.get(getPeerName("random")); random.enabled = false; @@ -388,6 +428,45 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishEvent", name + ".fsm")); + // loopbacks allow user to override or extend with python + listeners.add(new Listener("publishBoot", name)); + listeners.add(new Listener("publishHeartbeat", name)); + listeners.add(new Listener("publishConfigFinished", name)); + listeners.add(new Listener("publishStateChange", name)); + +// listeners.add(new Listener("publishPowerUp", name)); +// listeners.add(new Listener("publishPowerDown", name)); +// listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + // service --to--> InMoov2 + AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); + mouth_audioFile.listeners = new ArrayList<>(); + mouth_audioFile.listeners.add(new Listener("publishPeak", name)); + fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); + + + LogConfig log = (LogConfig) plan.get(getPeerName("log")); + log.listeners = new ArrayList<>(); + log.listeners.add(new Listener("publishLogEvents", name)); + +// mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); +// mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + + // InMoov2 --to--> service + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay")); + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + + listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel"))); + listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel"))); + // remove the auto-added starts in the plan's runtime RuntimConfig.registry plan.removeStartsWith(name + "."); diff --git a/src/main/java/org/myrobotlab/service/config/LogConfig.java b/src/main/java/org/myrobotlab/service/config/LogConfig.java new file mode 100644 index 0000000000..09f74a9481 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/LogConfig.java @@ -0,0 +1,12 @@ +package org.myrobotlab.service.config; + +public class LogConfig extends ServiceConfig { + + public String currentUserName; + + /** + * level of log error, warn, info, debug + */ + String level = "info"; + +} 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/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/meta/CalikoMeta.java b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java new file mode 100644 index 0000000000..61c6351aac --- /dev/null +++ b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java @@ -0,0 +1,41 @@ +package org.myrobotlab.service.meta; + +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.service.meta.abstracts.MetaData; +import org.slf4j.Logger; + +public class CalikoMeta extends MetaData { + private static final long serialVersionUID = 1L; + public final static Logger log = LoggerFactory.getLogger(CalikoMeta.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 CalikoMeta() { + + // add a cool description + addDescription("used as a general template"); + + // false will prevent it being seen in the ui + setAvailable(true); + + // add dependencies if necessary + // for the solver + addDependency("au.edu.federation.caliko", "caliko", "1.3.8"); + + // for the ui + addDependency("au.edu.federation.caliko.visualisation", "caliko-visualisation", "1.3.8"); + addDependency("au.edu.federation.caliko.visualisation", "caliko-demo", "1.3.8"); + + // add it to one or many categories + addCategory("ik", "inverse kinematics"); + + // add a sponsor to this service + // the person who will do maintenance + // setSponsor("GroG"); + + } + +} diff --git a/src/main/resources/resource/BoofCV/basket_depth.png b/src/main/resources/resource/BoofCV/basket_depth.png new file mode 100644 index 0000000000000000000000000000000000000000..14773053550348e3d3c492b36a03f064ef183d53 GIT binary patch literal 94310 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

    Gf&{_WYdlR?lG9R3-&Y-zW{JYon}h)w3_ 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�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

    f-FTN0YqlfoT~w~7I*`?Ldg*Z1P)U! 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

    Zo}wdPdBu$Kerl*iRNe1xq$`>>SqziCWX^ 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/resource/BoofCV/intrinsic.yaml b/src/main/resources/resource/BoofCV/intrinsic.yaml new file mode 100644 index 0000000000..db7c7f9ca9 --- /dev/null +++ b/src/main/resources/resource/BoofCV/intrinsic.yaml @@ -0,0 +1,19 @@ +# Pinhole camera model with radial and tangential distortion +# (fx,fy) = focal length, (cx,cy) = principle point, (width,height) = image shape +# radial = radial distortion, (t1,t2) = tangential distortion + +pinhole: + fx: 529.74137370586 + fy: 529.5715453060717 + cx: 312.57382117058427 + cy: 257.05061008728114 + width: 640 + height: 480 + skew: 0.0 +model: pinhole_radial_tangential +radial_tangential: + radial: + - 0.17889353480851655 + - -0.32301207366192053 + t1: 0.0 + t2: 0.0 diff --git a/src/main/resources/resource/BoofCV/visualdepth.yaml b/src/main/resources/resource/BoofCV/visualdepth.yaml new file mode 100644 index 0000000000..b911c9a497 --- /dev/null +++ b/src/main/resources/resource/BoofCV/visualdepth.yaml @@ -0,0 +1,20 @@ +# RGB Depth Camera Calibration +model: visual_depth +max_depth: 10000 +no_depth: 0 +intrinsic: + pinhole: + fx: 529.74137370586 + fy: 529.5715453060717 + cx: 312.57382117058427 + cy: 257.05061008728114 + width: 640 + height: 480 + skew: 0.0 + model: pinhole_radial_tangential + radial_tangential: + radial: + - 0.17889353480851655 + - -0.32301207366192053 + t1: 0.0 + t2: 0.0 \ No newline at end of file diff --git a/src/main/resources/resource/Caliko.png b/src/main/resources/resource/Caliko.png new file mode 100644 index 0000000000000000000000000000000000000000..3ddbc8369516b4d5f1b9ca6abcc43e80a2734878 GIT binary patch literal 1525 zcmVEX>4Tx04R}tkvmAkP!xv$riu?LB6d)5$WWauh>AFB6^c+H)C#RSnB4RQO&XFE z7e~Rh;NWAi>fqw6tAnc`2tGhuU7QqMq{MTRLW>v=T=@9!`48v*7Z7SCrde&{fTr7K zG9DAtnN_jl6k~y#OK6gCM`(($aTfzH_myN1wJ!urjql-VPY}g!b%IXf~gTt5l2)_r};wK zW0mt3XRTCWjXn7bLs@-gnd@|h5yv8yAVGwJ3W_Mfro4*KNwJWm`D&>*{h@IUySt(BXc@Fs;~K>Lg1e2f4cyFk6_IN!&PQ$GQM&%l-5^p|VE%qQv9 zrWQE@dbfd#>!zmc0hc?#z>^`HvMUAY2)P{aen#Jv0s3!&?lmvB#yw6SfD~nwxB(6h zfzdq8UiW!-XKQZ%_B8VQ0c0I=p}T?_fdBvi24YJ`L;(K){{a7>y{D4^000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2j>Y90WKsI=2Y7N000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}000BbNklrKhRPb)V{dB6uLRk*WyGg2&J@)j%^Qu4}I{VC}G`esW1c~c8XAaFtN22 zD_Rrl{Fi@s54yO}xy5yw1Bd(FbMO7`@0@cl_W+2xx;oL?+A4sIwYa!gG&MCHX^qKb z5{-?GX*NwqM~47#I2=fl#PaeonMz(>9xE#=8PC^fG}vr5!r^e5eUIkmP4QWEwPMTHY-Jot>S6M1p5NA35{$eDLZOi9~|!?d^=bGMmk8Zf<5A0i)6Q*1z{? z`uhcU?;a_qM^&rUDkMotvq>ooNdm1l)vcB}I87|F&Um4TvD2sd&hLkN_x>@zHk%Ev*NeyF zK`t*x@_Mn^Z0zjpu)Dj95CU12nVOnZ((Eh~ zrMbADhOpc1oRp*h1_uWTz8}LCG!czPPaXi^`Rq7L6=zWtB|89-z&O8u{}r_^S9SnK zMn-sX?i}vbRjRA2PaXi^Q9MpTB!WX#vjgz_;R08j`D1MolhN(>;M2v z>2!Q3MD_ss9zI0T>nXEZvjebl@gmb_&hT+>FI>HP!T|fv?r(lSSDripg8}Z`fyx3okenZ)gO^GR75AE_#Zg@w$_%p7?$gTX*7 z79*KV9s&>yg3kwfJqji;IiQ&(AY3FmNb<)oNvH zYm0b1{x1Lk7cRj0^AL-H*$jaI+`s>~rFKnC4dvzKsH#dyNeR(tlu#&y-|uH)Vj?wH zQT6~J6M%ORm&-+ceLbqG($&?KvIhcIR#&(1`QnFM!(-MPH>{$)J^dOUO9YN`2DD^Z bvJUVEs+jr2VLzv&00000NkvXXu0mjf5ADrM literal 0 HcmV?d00001 diff --git a/src/main/resources/resource/Caliko/pyramid.obj b/src/main/resources/resource/Caliko/pyramid.obj new file mode 100644 index 0000000000..797d3ac9fe --- /dev/null +++ b/src/main/resources/resource/Caliko/pyramid.obj @@ -0,0 +1,56 @@ +# Triangle consisting of 8 lines: A square at the bottom then from each vertex up to the tip + + +# Top Left Back +# v -0.5f 0.5f 0.0f +# Top Right Back +# v 0.5f 0.5f 0.0f +# Bottom Left Back +# v -0.5f -0.5f 0.0f +# Bottom Right Back +# v 0.5f -0.5f 0.0f +# Center Front +# v 0.0f 0.0f 1.0f + +# Top Left Back +v -0.5f 0.5f 0.0f +# Bottom Left Back +v -0.5f -0.5f 0.0f + +# Top Left Back +v -0.5f 0.5f 0.0f +# Top Right Back +v 0.5f 0.5f 0.0f + +# Top Right Back +v 0.5f 0.5f 0.0f +# Bottom Right Back +v 0.5f -0.5f 0.0f + +# Bottom Right Back +v 0.5f -0.5f 0.0f +# Bottom Left Back +v -0.5f -0.5f 0.0f + + + +# SQUARE +# Top Left Back +v -0.5f 0.5f 0.0f +# Center Front +v 0.0f 0.0f 1.0f + +# Top Right Back +v 0.5f 0.5f 0.0f +# Center Front +v 0.0f 0.0f 1.0f + +# Bottom Left Back +v -0.5f -0.5f 0.0f +# Center Front +v 0.0f 0.0f 1.0f + +# Bottom Right Back +v 0.5f -0.5f 0.0f +# Center Front +v 0.0f 0.0f 1.0f \ No newline at end of file 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/ServoMixerTest.java b/src/test/java/org/myrobotlab/service/ServoMixerTest.java new file mode 100644 index 0000000000..da5c35683b --- /dev/null +++ b/src/test/java/org/myrobotlab/service/ServoMixerTest.java @@ -0,0 +1,31 @@ +package org.myrobotlab.service; + +import org.junit.Test; +import org.myrobotlab.kinematics.Gesture; +import org.myrobotlab.kinematics.GesturePart; +import org.myrobotlab.test.AbstractTest; + +public class ServoMixerTest extends AbstractTest { + + @Test + public void testService() throws Exception { + + Servo s1 = (Servo)Runtime.start("s1", "Servo"); + Servo s2 = (Servo)Runtime.start("s2", "Servo"); + + ServoMixer mixer = (ServoMixer)Runtime.start("mixer", "ServoMixer"); + String gestureFileName = "mixerTest-1"; + // mixer.addNewGestureFile(gestureFileName); + Gesture gesture = new Gesture(); + + s1.moveTo(5); + s2.moveTo(5); + mixer.savePose("pose-1"); + + GesturePart part1 = new GesturePart(); + + gesture.getParts().add(null); + + } + +} \ No newline at end of file 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)); + } + +} From cc329accf6eabc8b53908417eeec36b54fdf1d4d Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:32:51 -0700 Subject: [PATCH 031/232] transfer2 --- pom.xml | 971 +++++++++--------- .../org/myrobotlab/kinematics/DHRobotArm.java | 5 +- .../service/FiniteStateMachine.java | 15 + src/main/java/org/myrobotlab/service/Git.java | 203 ++-- .../java/org/myrobotlab/service/InMoov2.java | 948 +++++++++++++---- .../org/myrobotlab/service/InMoov2Head.java | 37 +- .../service/InverseKinematics3D.java | 9 +- .../org/myrobotlab/service/JMonkeyEngine.java | 4 +- src/main/java/org/myrobotlab/service/Log.java | 12 +- .../java/org/myrobotlab/service/NeoPixel.java | 50 + .../java/org/myrobotlab/service/OakD.java | 40 +- src/main/java/org/myrobotlab/service/Pir.java | 68 +- .../java/org/myrobotlab/service/Random.java | 33 +- .../java/org/myrobotlab/service/Updater.java | 98 +- .../java/org/myrobotlab/service/WebXR.java | 155 +++ .../service/config/InMoov2Config.java | 111 +- .../org/myrobotlab/vertx/ApiVerticle.java | 97 ++ .../myrobotlab/vertx/WebSocketHandler.java | 108 ++ 18 files changed, 2099 insertions(+), 865 deletions(-) create mode 100644 src/main/java/org/myrobotlab/service/WebXR.java create mode 100644 src/main/java/org/myrobotlab/vertx/ApiVerticle.java create mode 100644 src/main/java/org/myrobotlab/vertx/WebSocketHandler.java diff --git a/pom.xml b/pom.xml index a1507381a2..259b4e77b4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,102 +1,102 @@ - - - 4.0.0 - org.myrobotlab - mrl - 0.0.1-SNAPSHOT - MyRobotLab - Open Source Creative Machine Control - - - false - - - - 1.1. - - ${maven.build.timestamp} - yyyyMMddHHmm - ${timestamp} - ${version.prefix}${build.number} - ${git.branch} - ${NODE_NAME} - ${NODE_LABELS} - - - - 11 - 11 - UTF-8 - - - + + + 4.0.0 + org.myrobotlab + mrl + 0.0.1-SNAPSHOT + MyRobotLab + Open Source Creative Machine Control + + + false + + + + 1.1. + + ${maven.build.timestamp} + yyyyMMddHHmm + ${timestamp} + ${version.prefix}${build.number} + ${git.branch} + ${NODE_NAME} + ${NODE_LABELS} + + + + 11 + 11 + UTF-8 + + + @@ -135,9 +135,9 @@ https://m2.dv8tion.net/releases - - - + + + javazoom @@ -1646,6 +1646,18 @@ + + au.edu.federation.caliko + caliko + 1.3.8 + + + + au.edu.federation.caliko.visualisation + caliko-visualisation + 1.3.8 + + com.github.sarxos @@ -1734,375 +1746,386 @@ - - - org.mockito - mockito-core - 3.12.4 - test - - - - - - - false - src/main/resources - - - false - src/main/java - - ** - - - **/*.java - - - - - - false - src/test/resources - - - false - src/test/java - - ** - - - **/*.java - - - - src/main/resources - ${project.basedir} - - - - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0.0 - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.1.0 - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - no-duplicate-declared-dependencies - - enforce - - - - - - - - - - - - org.codehaus.mojo - properties-maven-plugin - - - initialize - - read-project-properties - - - - build.properties - - - - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.1.0 - - - package - - shade - - - myrobotlab - - true - myrobotlab-full - false - - - - - org.myrobotlab.service.Runtime - ${version} - ${version} - - ${build.number} - ${maven.build.timestamp} - ${agent.name} - ${user.name} - - - ${git.tags} - ${git.branch} - ${git.dirty} - ${git.remote.origin.url} - ${git.commit.id} - ${git.commit.id.abbrev} - ${git.commit.id.full} - ${git.commit.id.describe} - ${git.commit.id.describe-short} - ${git.commit.user.name} - ${git.commit.user.email} - - ${git.commit.time} - ${git.closest.tag.name} - ${git.closest.tag.commit.count} - ${git.build.user.name} - ${git.build.user.email} - ${git.build.time} - ${git.build.version} - - - - - - - *:* - - module-info.class - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - assembly.xml - - myrobotlab - false - - - - trigger-assembly - package - - single - - - - - - - true - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 11 - 11 - true - true - -parameters - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.4.3 - - - - pl.project13.maven - git-commit-id-plugin - 4.9.10 - - - initialize - get-the-git-infos - - revision - - - - - ${project.basedir}/.git - git - false - true - ${project.build.outputDirectory}/git.properties - - - false - false - -dirty - - - - - - maven-surefire-plugin - org.apache.maven.plugins - 2.22.2 - - -Djava.library.path=libraries/native -Djna.library.path=libraries/native - - **/*Test.java - - - **/integration/* - - - - - - - - org.apache.maven.plugins - maven-clean-plugin - 2.3 - - - - data/.myrobotlab - false - - - libraries - - ** - - false - - - data - - ** - - - - resource - - ** - - - - src/main/resources/resource/framework - - **/serviceData.json - - false - - - - - - - - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.22.2 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - - - myrobotlab - http://myrobotlab.org - - - github - https://github.com/MyRobotLab/myrobotlab/issues - - + + dev.onvoid.webrtc + webrtc-java + 0.7.0 + + + + + org.mockito + mockito-core + 3.12.4 + test + + + au.edu.federation.caliko.demo + caliko-demo + 1.3.8 + + + + + + + false + src/main/resources + + + false + src/main/java + + ** + + + **/*.java + + + + + + false + src/test/resources + + + false + src/test/java + + ** + + + **/*.java + + + + src/main/resources + ${project.basedir} + + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.1.0 + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + no-duplicate-declared-dependencies + + enforce + + + + + + + + + + + + org.codehaus.mojo + properties-maven-plugin + + + initialize + + read-project-properties + + + + build.properties + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + myrobotlab + + true + myrobotlab-full + false + + + + + org.myrobotlab.service.Runtime + ${version} + ${version} + + ${build.number} + ${maven.build.timestamp} + ${agent.name} + ${user.name} + + + ${git.tags} + ${git.branch} + ${git.dirty} + ${git.remote.origin.url} + ${git.commit.id} + ${git.commit.id.abbrev} + ${git.commit.id.full} + ${git.commit.id.describe} + ${git.commit.id.describe-short} + ${git.commit.user.name} + ${git.commit.user.email} + + ${git.commit.time} + ${git.closest.tag.name} + ${git.closest.tag.commit.count} + ${git.build.user.name} + ${git.build.user.email} + ${git.build.time} + ${git.build.version} + + + + + + + *:* + + module-info.class + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + assembly.xml + + myrobotlab + false + + + + trigger-assembly + package + + single + + + + + + + true + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 11 + 11 + true + true + -parameters + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + + + pl.project13.maven + git-commit-id-plugin + 4.9.10 + + + initialize + get-the-git-infos + + revision + + + + + ${project.basedir}/.git + git + false + true + ${project.build.outputDirectory}/git.properties + + + false + false + -dirty + + + + + + maven-surefire-plugin + org.apache.maven.plugins + 2.22.2 + + -Djava.library.path=libraries/native -Djna.library.path=libraries/native + + **/*Test.java + + + **/integration/* + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 2.3 + + + + data/.myrobotlab + false + + + libraries + + ** + + false + + + data + + ** + + + + resource + + ** + + + + src/main/resources/resource/framework + + **/serviceData.json + + false + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + + + myrobotlab + http://myrobotlab.org + + + github + https://github.com/MyRobotLab/myrobotlab/issues + + diff --git a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java index 03b8c251b9..6fd3e43b13 100644 --- a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java +++ b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java @@ -235,7 +235,10 @@ public boolean moveToGoal(Point goal) { int numSteps = 0; double iterStep = 0.05; // we're in millimeters.. - double errorThreshold = 2.0; + double errorThreshold = 20.0; + + maxIterations = 1000; + // what's the current point while (true) { numSteps++; diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index a093f61565..e82bdfe744 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -58,6 +58,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(); diff --git a/src/main/java/org/myrobotlab/service/Git.java b/src/main/java/org/myrobotlab/service/Git.java index 1d2d3766f5..8832326052 100644 --- a/src/main/java/org/myrobotlab/service/Git.java +++ b/src/main/java/org/myrobotlab/service/Git.java @@ -3,7 +3,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Properties; @@ -31,6 +30,7 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.submodule.SubmoduleWalk; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Service; import org.myrobotlab.logging.Level; @@ -45,7 +45,7 @@ public class Git extends Service { public final static Logger log = LoggerFactory.getLogger(Git.class); - transient static TextProgressMonitor monitor = new TextProgressMonitor(); + transient ProgressMonitor monitor = new ProgressMonitor(); Map repos = new TreeMap<>(); @@ -56,76 +56,43 @@ public static class RepoData { String branch; String location; String url; - List branches; String checkout; transient org.eclipse.jgit.api.Git git; - public RepoData(String location, String url, List branches, String checkout, org.eclipse.jgit.api.Git git) { + public RepoData(String location, String url, String checkout, org.eclipse.jgit.api.Git git) { this.location = location; this.url = url; - this.branches = branches; this.checkout = checkout; this.git = git; } } + // TODO - overload updates to publish + public class ProgressMonitor extends TextProgressMonitor { + + } + public Git(String n, String id) { super(n, id); } - - // max complexity clone - public void clone(String location, String url, List inbranches, String incheckout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - - File repoLocation = new File(location); - org.eclipse.jgit.api.Git git = null; - Repository repo = null; - - List branches = new ArrayList<>(); - for (String b : inbranches) { - if (!b.contains("refs")) { - branches.add("refs/heads/" + b); - } - } - - String checkout = (incheckout.contains("refs")) ? incheckout : "refs/heads/" + incheckout; - - if (!repoLocation.exists()) { - // clone - log.info("cloning {} {} into {}", url, incheckout, location); - git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranchesToClone(branches).setBranch(checkout).call(); - - } else { - // Open an existing repository - String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; - log.info("opening repo {} from {}", gitDir, url); - repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); - git = new org.eclipse.jgit.api.Git(repo); - } - - repo = git.getRepository(); - - // checkout - log.info("checking out {}", incheckout); - // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" - // + incheckout).call(); - git.branchCreate().setForce(true).setName(incheckout).setStartPoint(incheckout).call(); - git.checkout().setName(incheckout).call(); - - repos.put(location, new RepoData(location, url, inbranches, incheckout, git)); - + + public void clone(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + clone(location, url, branch, checkout, false); } + + // max complexity sync - public void sync(String location, String url, List branches, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + public void sync(String location, String url, String branch, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { // initial clone - clone(location, url, branches, checkout); + clone(location, url, branch, checkout); addTask(checkStatusIntervalMs, "checkStatus"); } public void sync(String location, String url, String checkout) throws InvalidRemoteException, TransportException, GitAPIException, IOException { - sync(location, url, Arrays.asList(checkout), checkout); + sync(location, url, checkout, checkout); } public RevCommit checkStatus() throws WrongRepositoryStateException, InvalidConfigurationException, InvalidRemoteException, CanceledException, RefNotFoundException, @@ -167,7 +134,7 @@ public RevCommit publishPull(RevCommit commit) { return commit; } - private static List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { List ret = new ArrayList<>(); Repository repository = git.getRepository(); @@ -193,17 +160,17 @@ public void stopSync() { purgeTasks(); } - static public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, - RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { + public int pull() throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, + NoHeadException, TransportException, IOException, GitAPIException { return pull(null, null); } - static public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, + public int pull(String branch) throws WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, IOException, GitAPIException { return pull(null, branch); } - static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { + org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -224,7 +191,7 @@ static org.eclipse.jgit.api.Git getGit(String rootFolder) throws IOException { return git; } - static public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { if (src == null) { @@ -278,11 +245,11 @@ static public int pull(String src, String branch) throws IOException, WrongRepos return 0; } - static public void init() throws IllegalStateException, GitAPIException { + public void init() throws IllegalStateException, GitAPIException { init(null); } - static public void init(String directory) throws IllegalStateException, GitAPIException { + public void init(String directory) throws IllegalStateException, GitAPIException { if (directory == null) { directory = System.getProperty("user.dir"); } @@ -291,55 +258,30 @@ static public void init(String directory) throws IllegalStateException, GitAPIEx org.eclipse.jgit.api.Git git = org.eclipse.jgit.api.Git.init().setDirectory(dir).call(); } - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.INFO); - - Properties properties = Platform.gitProperties(); - Git.removeProps(); - log.info("{}", properties); - - /* - * // start the service Git git = (Git) Runtime.start("git", "Git"); - * - * // check out and sync every minute // git.sync("test", - * "https://github.com/MyRobotLab/WorkE.git", "master"); // - * git.sync("/lhome/grperry/github/mrl/myrobotlab", // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - * git.gitPull("agent-removal"); // - * git.sync(System.getProperty("user.dir"), // - * "https://github.com/MyRobotLab/myrobotlab.git", "agent-removal"); - */ - } catch (Exception e) { - log.error("main threw", e); - } - } - - public static String getBranch() throws IOException { + public String getBranch() throws IOException { return getBranch(null); } - public static String getBranch(String src) throws IOException { + public String getBranch(String src) throws IOException { org.eclipse.jgit.api.Git git = getGit(src); return git.getRepository().getBranch(); } - public static Status status() throws NoWorkTreeException, IOException, GitAPIException { + public Status status() throws NoWorkTreeException, IOException, GitAPIException { return status(null); } - public static Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { + public Status status(String src) throws IOException, NoWorkTreeException, GitAPIException { org.eclipse.jgit.api.Git git = getGit(src); Status status = git.status().call(); return status; } - public static void removeProps() { + public void removeProps() { removeProps(null); } - public static void removeProps(String rootFolder) { + public void removeProps(String rootFolder) { if (rootFolder == null) { rootFolder = System.getProperty("user.dir"); } @@ -349,4 +291,89 @@ public static void removeProps(String rootFolder) { } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.INFO); + + Properties properties = Platform.gitProperties(); + // Git.removeProps(); + log.info("{}", properties); + Git git = (Git) Runtime.start("git", "Git"); + git.clone("./depthai", "https://github.com/luxonis/depthai.git", "main", "refs/tags/v1.13.1-sdk", true); + log.info("here"); + + + } catch (Exception e) { + log.error("main threw", e); + } + } + + // max complexity clone and checkout + public void clone(String location, String url, String branch, String checkout, boolean recursive) throws InvalidRemoteException, TransportException, GitAPIException, IOException { + + File repoLocation = new File(location); + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // git clone + + if (!repoLocation.exists()) { + // clone + log.info("cloning {} {} checking out {} into {}", url, branch, checkout, location); + git = org.eclipse.jgit.api.Git.cloneRepository().setProgressMonitor(monitor).setURI(url).setDirectory(repoLocation).setBranch(branch).call(); + + } else { + // Open an existing repository + String gitDir = repoLocation.getAbsolutePath() + File.separator + ".git"; + log.info("opening repo {} from {}", gitDir, url); + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + } + + repo = git.getRepository(); + + // git pull + + PullCommand pullCmd = git.pull() + // .setRemote(remoteName) + // .setCredentialsProvider(new + // UsernamePasswordCredentialsProvider(username, password)) + .setRemoteBranchName(branch); + + // Perform the pull operation + pullCmd.call(); + + + // recursive + if (recursive) { + + // Recursively fetch and checkout submodules if they exist + SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repo); + while (submoduleWalk.next()) { + String submodulePath = submoduleWalk.getPath(); + org.eclipse.jgit.api.Git submoduleGit = org.eclipse.jgit.api.Git.open(new File(location, submodulePath)); + submoduleGit.fetch() + .setRemote("origin") + .call(); + submoduleGit.checkout() + .setName(branch) // Replace with the desired branch name + .call(); + } + + } + + if (checkout != null) { + // checkout + log.info("checking out {}", checkout); + // git.branchCreate().setForce(true).setName(incheckout).setStartPoint("origin/" + // + incheckout).call(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(checkout).call(); + git.checkout().setName(checkout).call(); + } + + repos.put(location, new RepoData(location, url, checkout, git)); + + } + } diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 51dac5ae44..93ffd39f0f 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -25,10 +25,11 @@ import org.myrobotlab.opencv.OpenCVData; import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; +import org.myrobotlab.service.Log.LogEntry; import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.data.Event; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; @@ -54,6 +55,8 @@ public class InMoov2 extends Service implements ServiceLifeCycleL static String speechRecognizer = "WebkitSpeechRecognition"; + protected static final Set stateDefaults = new TreeSet<>(); + /** * This method will load a python file into the python interpreter. * @@ -93,6 +96,23 @@ public static boolean loadFile(String file) { return true; } + public static void main(String[] args) { + try { + + LoggingFactory.init(Level.ERROR); + // identical to command line start + // Runtime.startConfig("inmoov2"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + } catch (Exception e) { + log.error("main threw", e); + } + } + + /** + * the config that was processed before booting, if there was one. + */ + String bootedConfig = null; + protected transient ProgramAB chatBot; protected List configList; @@ -103,16 +123,35 @@ public static boolean loadFile(String file) { */ protected boolean configStarted = false; - String currentConfigurationName = "default"; + /** + * map of events or states to sounds + */ + protected Map customSoundMap = new TreeMap<>(); protected transient SpeechRecognizer ear; + protected List errors = new ArrayList<>(); + + /** + * The finite state machine is core to managing state of InMoov2. There is + * very little benefit gained in having the interactions pub/sub. Therefore, + * there will be a direct reference to the fsm. If different state graph is + * needed, then the fsm can provide that service. + */ + private transient FiniteStateMachine fsm = null; // waiting controable threaded gestures we warn user protected boolean gestureAlreadyStarted = false; protected Set gestures = new TreeSet(); + /** + * Prevents actions or events from happening when InMoov2 is first booted + */ + private boolean hasBooted = false; + + protected boolean isPirOn = false; + protected transient HtmlFilter htmlFilter; protected transient ImageDisplay imageDisplay; @@ -121,28 +160,66 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected LedDisplayData led = new LedDisplayData(); + protected Map ledDisplayMap = new TreeMap<>(); /** * supported locales */ protected Map locales = null; - protected int maxInactivityTimeSeconds = 120; - protected transient SpeechSynthesis mouth; protected boolean mute = false; protected transient OpenCV opencv; + protected List peersStarted = new ArrayList<>(); + protected transient Python python; + protected long stateLastIdleTime = System.currentTimeMillis(); + + protected long stateLastRandomTime = System.currentTimeMillis(); + protected String voiceSelected; + protected boolean wasMutedBeforeBoot = false; public InMoov2(String n, String id) { super(n, id); + + // add the default InMoov2 state handlers - so the FSM can invoke them + // this is hardcode, because it requires Java methods in InMoov2 + // so it makes sense to hardcode them... + // if a user needs something different, it will happen in pyton-land + // consequence it this will need maintenance if there are new InMoov2 java + // state handlers + stateDefaults.add("wake"); + stateDefaults.add("firstInit"); + stateDefaults.add("idle"); + stateDefaults.add("random"); + stateDefaults.add("sleep"); // listens & dreams, no opencv, waits for + // wakeWord, pir active + stateDefaults.add("powerDown"); // stops heartbeat, listening ? + stateDefaults.add("shutdown");// ends mrl + + ledDisplayMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); + ledDisplayMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); + ledDisplayMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); + ledDisplayMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); + ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); + ledDisplayMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); + ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); + + customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); + customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); + customSoundMap.put("idle", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/start.wav")); + customSoundMap.put("random", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/reveal.wav")); + customSoundMap.put("sleep", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/back.wav")); + customSoundMap.put("powerDown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); + customSoundMap.put("shutdown", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/marimba.wav")); + } public void addTextListener(TextListener service) { @@ -451,9 +528,46 @@ public void finishedGesture(String nameOfGesture) { } } - // FIXME - this isn't the callback for fsm - why is it needed here ? - public void fire(String event) { - invoke("publishEvent", event); + public void firstInit() { + log.info("firstInit"); + // cheap way to prevent race condition + // of "wake" firing a state change .. which will spawn + // a system event of FIRST_INIT that will answer this + // question ... + sleep(2000); + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); + if (chatBot != null) { + chatBot.getResponse("FIRST_INIT"); + } + } + + public void flash(String name) { + LedDisplayData led = ledDisplayMap.get(name); + if (led == null) { + led = ledDisplayMap.get("default"); + } + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + + /** + * used to configure a flashing event - could use configuration to signal + * different colors and states + * + * @return + */ + public void flash() { + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); + } + } + + public void flash(int r, int g, int b, int count) { + // FIXME - this should be checking a protected "state" + if (ledDisplayMap.get("default") != null) { + LedDisplayData led = ledDisplayMap.get("default"); + invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); + } } public void fullSpeed() { @@ -572,6 +686,10 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } + public String getState() { + return fsm.getCurrent(); + } + /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -616,12 +734,10 @@ public void halfSpeed() { } /** - * execute python scripts in the init directory on startup of the service - * - * @throws IOException + * pir active ear listening for wakeword */ - public void loadInitScripts() throws IOException { - loadScripts(getResourceDir() + fs + "init"); + public void idle() { + log.info("idle"); } public boolean isCameraOn() { @@ -815,6 +931,115 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "moveToBlocking", topStom, midStom, lowStom); } + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + synchronized public void onBoot() { + + // thinking you shouldn't "boot" twice ? + if (hasBooted) { + log.warn("will not boot again"); + return; + } + + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) + try { + // copy config if it doesn't already exist + String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + List files = FileIO.getFileList(resourceBotDir); + for (File f : files) { + String botDir = "data/config/" + f.getName(); + File bDir = new File(botDir); + if (bDir.exists() || !f.isDirectory()) { + log.info("skipping data/config/{}", botDir); + } else { + log.info("will copy new data/config/{}", botDir); + try { + FileIO.copy(f.getAbsolutePath(), botDir); + } catch (Exception e) { + error(e); + } + } + } + } catch (Exception e) { + error(e); + } + + // FIXME - find good way of running an animation "through" a state + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + invoke("publishPlayAnimation", config.bootAnimation); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")).playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG STARTED %s", bootedConfig); + } + + for (String peerKey : peersStarted) { + systemEvent("STARTED %s", peerKey); + } + + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + + fsm.fire("wake"); + + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + + hasBooted = true; + } + public PredicateEvent onChangePredicate(PredicateEvent event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { @@ -843,12 +1068,6 @@ public void onCreated(String fullname) { log.info("{} created", fullname); } - public void onFinishedConfig(String configName) { - log.info("onFinishedConfig"); - // invoke("publishEvent", "configFinished"); - invoke("publishFinishedConfig", configName); - } - public void onGestureStatus(Status status) { if (!status.equals(Status.success()) && !status.equals(Status.warn("Python process killed !"))) { error("I cannot execute %s, please check logs", lastGestureExecuted); @@ -858,6 +1077,87 @@ public void onGestureStatus(Status status) { unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + /** + * A generalized recurring event which can preform checks and various other + * methods or tasks. Heartbeats will not start until after boot stage. + */ + public void onHeartbeat() { + try { + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + + if (!hasBooted) { + log.info("boot hasn't completed, will not process heartbeat"); + return; + } + + Long lastActivityTime = getLastActivityTime(); + + // FIXME lastActivityTime != 0 is bogus - the value should be null if + // never set + if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0 + && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } + + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fsm.fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } + + // interval event firing + if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + // fsm.fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } + + } catch (Exception e) { + error(e); + } + + if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) { + flash("pirOn"); + } + + if (config.batteryLevelCheck) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } + } + + // flash error until errors are cleared + if (config.healthCheckFlash) { + if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { + invoke("publishFlash", ledDisplayMap.get("error")); + } else if (ledDisplayMap.containsKey("heartbeat")) { + LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); + invoke("publishFlash", heartbeat); + } + } + + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + + } + + /** + * Central hub of input motion control. Potentially, all input from + * joysticks, quest2 controllers and headset, or any IK service could + * be sent here + */ @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -878,21 +1178,66 @@ public void onJoystickInput(JoystickData input) throws Exception { invoke("publishEvent", "joystick"); } - public String onNewState(String state) { - log.error("onNewState {}", state); + /** + * Centralized logging system will have all logging from all services, + * including lower level logs that do not propegate as statuses + * + * @param log + * - flushed log from Log service + */ + public void onLogEvents(List log) { + // scan for warn or errors + for (LogEntry entry : log) { + if ("ERROR".equals(entry.level) && errors.size() < 100) { + errors.add(entry); + } + } + } - // put configurable filter here ! + public void onMoveHead(Map map) { + InMoov2Head head = (InMoov2Head) getPeer("head"); + if (head != null) { + head.onMove(map); + } + } - // state substitutions ? - // let python subscribe directly to fsm.publishNewState + public void onMoveLeftArm(Map map) { + InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); + if (leftArm != null) { + leftArm.onMove(map); + } + } - // if - invoke(state); - // depending on configuration .... - // call python ? - // fire fsm events ? - // do defaults ? - return state; + public void onMoveLeftHand(Map map) { + InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); + if (leftHand != null) { + leftHand.onMove(map); + } + } + + // public Message publishPython(String method, Object...data) { + // return Message.createMessage(getName(), getName(), method, data); + // } + + public void onMoveRightArm(Map map) { + InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); + if (rightArm != null) { + rightArm.onMove(map); + } + } + + public void onMoveRightHand(Map map) { + InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); + if (rightHand != null) { + rightHand.onMove(map); + } + } + + public void onMoveTorso(Map map) { + InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); + if (torso != null) { + torso.onMove(map); + } } public OpenCVData onOpenCVData(OpenCVData data) { @@ -901,35 +1246,35 @@ public OpenCVData onOpenCVData(OpenCVData data) { } /** - * initial callback for Pir sensor Default behavior will be: send fsm event - * onPirOn flash neopixel + * onPeak volume callback TODO - maybe make it variable with volume ? + * + * @param volume + */ + public void onPeak(double volume) { + if (config.neoPixelFlashWhenSpeaking && !configStarted) { + if (volume > 0.5) { + if (ledDisplayMap.get("onPeakColor") != null) { + LedDisplayData onPeakColor = ledDisplayMap.get("onPeakColor"); + invoke("publishFlash", onPeakColor); + } + } + } + } + + /** + * Pir on callback */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.interval = 500; - // FIXME flash on config.flashOnBoot - invoke("publishFlash"); - // pirOn event vs wake event - invoke("publishEvent", "WAKE"); + isPirOn = true; + fsm.fire("wake"); } - // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python - // yet provides a stable default, which can be fully replaced - // Works using common pub/sub rules - // TODO - this is a loopback power up - // its replaceable by typical subscription rules - public void onPowerUp() { - // CON - type aware - NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); - // CON - necessary NPE checking - if (neoPixel != null) { - neoPixel.setColor(0, 130, 0); - neoPixel.playAnimation("Larson Scanner"); - } + /** + * Pir off callback + */ + public void onPirOff() { + isPirOn = false; + fsm.fire("sleep"); } @Override @@ -1101,6 +1446,81 @@ public void onStarted(String name) { } } + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param stateChange + * @return + */ + public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) { + try { + log.error("onStateChange {}", stateChange); + + String current = stateChange.current; + String last = stateChange.last; + + // leaving random state + if ("random".equals(last) && !"random".equals(current) && isPeerStarted("random")) { + Random random = (Random) getPeer("random"); + random.disable(); + } + + if ("wake".equals(last)) { + invoke("publishStopAnimation"); + } + + if (config.systemEventStateChange) { + systemEvent("ON STATE %s", current); + } + + if (config.customSounds && customSoundMap.containsKey(current)) { + invoke("publishPlayAudioFile", customSoundMap.get(current)); + } + + // TODO - only a few InMoov2 state defaults will be called here + if (stateDefaults.contains(current)) { + invoke(current); + } + + // FIXME add topic changes to AIML here ! + // FIXME add clallbacks to inmmoov2 library + + // put configurable filter here ! + + // state substitutions ? + // let python subscribe directly to fsm.publishStateChange + + // if python && configured to do python inmoov2 library callbacks + // do a callback ... default NOOPs should be in library + + // if + // invoke(state); + // depending on configuration .... + // call python ? + // fire fsm events ? + // do defaults ? + } catch (Exception e) { + error(e); + } + return stateChange; + } + @Override public void onStopped(String name) { // using release peer for peer releasing @@ -1117,36 +1537,25 @@ public void onText(String text) { invoke("publishText", text); } - // TODO FIX/CHECK this, migrate from python land public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) rest(); - purgeTasks(); + purgeTasks(); // including heartbeat disable(); - if (ear != null) { - ear.lockOutAllGrammarExcept("power up"); + if (chatBot != null) { + chatBot.sleep(); } - // FIXME - DO NOT DO THIS !!!! SIMPLY PUBLISH A POWER DOWN EVENT AND PYTHON - // CAN SUBSCRIBE - // AND MAINTAIN A SET OF onPowerDown: callback methods - python.execMethod("power_down"); - } - - // TODO FIX/CHECK this, migrate from python land - // FIXME - defaultPowerUp switchable + override - public void powerUp() { - enable(); - rest(); - if (ear != null) { - ear.clearLock(); + // FIXME - bad remove it - what is needed ? + // i think this is legacy wake word + ear.lockOutAllGrammarExcept("power up"); } - beginCheckingOnInactivity(); - - python.execMethod("power_up"); } /** @@ -1167,10 +1576,11 @@ public String publishStartConfig(String configName) { return configName; } - public String publishFinishedConfig(String configName) { - info("config %s finished", configName); - invoke("publishEvent", "CONFIG LOADED " + configName); + public void publishBoot() { + log.info("publishBoot"); + } + public String publishConfigFinished(String configName) { return configName; } @@ -1184,15 +1594,19 @@ public List publishConfigList() { return configList; } - /** - * event publisher for the fsm - although other services potentially can - * consume and filter this event channel - * - * @param event - * @return - */ - public String publishEvent(String event) { - return String.format("SYSTEM_EVENT %s", event); + public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + return data; + } + + public LedDisplayData publishFlash(LedDisplayData data) { + return data; } /** @@ -1217,8 +1631,17 @@ public String publishHeartbeat() { } /** - * A more extensible interface point than publishEvent - * FIXME - create interface for this + * if inactivityTime configured, this event is published after there has not + * been in activity since. + */ + public void publishInactivity() { + log.info("publishInactivity"); + fsm.fire("inactvity"); + } + + /** + * A more extensible interface point than publishEvent FIXME - create + * interface for this * * @param msg * @return @@ -1298,23 +1721,36 @@ public HashMap publishMoveRightArm(Double bicep, Double rotate, return map; } - public HashMap publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - return map; + public String publishPlayAudioFile(String filename) { + return filename; } - public HashMap publishMoveTorso(Double topStom, Double midStom, Double lowStom) { - HashMap map = new HashMap<>(); - map.put("topStom", topStom); - map.put("midStom", midStom); - map.put("lowStom", lowStom); - return map; + public String publishPlayAnimation(String animation) { + return animation; + } + + /** + * stop animation event + */ + public void publishStopAnimation() { + } + + public FiniteStateMachine.StateChange publishStateChange(FiniteStateMachine.StateChange state) { + log.info("publishStateChange {}", state); + return state; + } + + /** + * event publisher for the fsm - although other services potentially can + * consume and filter this event channel + * + * @param event + * @return + */ + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh + return String.format("SYSTEM_EVENT %s", event); } /** @@ -1325,6 +1761,16 @@ public String publishText(String text) { return text; } + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + @Override public void releasePeer(String peerKey) { super.releasePeer(peerKey); @@ -1532,6 +1978,11 @@ public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist); } + // ----------------------------------------------------------------------------- + // These are methods added that were in InMoov1 that we no longer had in + // InMoov2. + // From original InMoov1 so we don't loose the + public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); } @@ -1562,10 +2013,17 @@ public void setVoice(String name) { } } - // ----------------------------------------------------------------------------- - // These are methods added that were in InMoov1 that we no longer had in - // InMoov2. - // From original InMoov1 so we don't loose the + public void shutdown() { + log.info("shutdown"); + Runtime.shutdown(); + } + + /** + * ear still listening pir still active + */ + public void sleep() { + log.info("sleep"); + } public void sleeping() { log.error("sleeping"); @@ -1790,53 +2248,33 @@ public void startService() { // get service start and release life cycle events runtime.attachServiceLifeCycleListener(getName()); - - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); - } - } - - // get events of new services and shutdown - subscribe("runtime", "shutdown"); - // power up loopback subscription - addListener(getName(), "powerUp"); - - subscribe("runtime", "publishConfigList"); - if (runtime.isProcessingConfig()) { - invoke("publishEvent", "configStarted"); - } - subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); - // chatbot getresponse attached to publishEvent - addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + // subscribe to config processing events + // runtime callbacks publish the same a local + subscribe("runtime", "publishConfigStarted", "publishConfigStarted"); + subscribe("runtime", "publishConfigFinished", "publishConfigFinished"); - try { - // copy config if it doesn't already exist - String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); - List files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/config/" + f.getName(); - File bDir = new File(botDir); - if (bDir.exists() || !f.isDirectory()) { - log.info("skipping data/config/{}", botDir); - } else { - log.info("will copy new data/config/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } + runtime.invoke("publishConfigList"); + + // iterate through existing started service + // add them to peers booted + for (String name : Runtime.getServiceNames()) { + String peerKey = getPeerKey(name); + if (peerKey != null) { + peersStarted.add(peerKey); } - } catch (Exception e) { - error(e); } - runtime.invoke("publishConfigList"); + if (runtime.isProcessingConfig()) { + // if InMoov2 was started as part of a config set + // set here so boot can be delayed until the config + // set is done + configStarted = true; + bootedConfig = runtime.getConfigName(); + } else { + invoke("publishBoot"); + } } public void startServos() { @@ -1870,6 +2308,7 @@ public void stopGesture() { public void stopHeartbeat() { purgeTask("publishHeartbeat"); + config.heartbeat = false; } public void stopNeopixelAnimation() { @@ -1896,7 +2335,17 @@ public void systemCheck() { Platform platform = Runtime.getPlatform(); setPredicate("system version", platform.getVersion()); // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + systemEvent("SYSTEMCHECKFINISHED"); // wtf is this? + } + + public String systemEvent(String eventMsg) { + invoke("publishSystemEvent", eventMsg); + return eventMsg; + } + + public String systemEvent(String format, Object... ags) { + String eventMsg = String.format(format, ags); + return systemEvent(eventMsg); } // FIXME - if this is really desired it will drive local references for all @@ -1911,89 +2360,156 @@ public void waitTargetPos() { sendToPeer("torso", "waitTargetPos"); } + public void closeRightHand() { - public static void main(String[] args) { - try { - - LoggingFactory.init(Level.ERROR); - // Platform.setVirtual(true); - // Runtime.start("s01", "Servo"); - // Runtime.start("intro", "Intro"); - - // Runtime.startConfig("pr-1213-1"); - - Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); - - boolean done = true; - if (done) { - return; - } - + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - // webgui.setSsl(true); - webgui.autoStartBrowser(false); - // webgui.setPort(8888); - webgui.startService(); + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveRightHand", map); - Runtime.start("python", "Python"); - // Runtime.start("ros", "Ros"); - Runtime.start("intro", "Intro"); - // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - // i01.startPeer("simulator"); - // Runtime.startConfig("i01-05"); - // Runtime.startConfig("pir-01"); + } - // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); - // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); + public void openRightHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveRightHand", map); + } + + public void closeLeftHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveLeftHand", map); - // polly.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); - // i01.startPeer("mouth"); - // i01.speakBlocking("Hi, to be or not to be that is the question, - // wheather to take arms against a see of trouble, and by aposing them end - // them, to sleep, to die"); + } - Runtime.start("python", "Python"); + public void openLeftHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way - // i01.startSimulator(); - Plan plan = Runtime.load("webgui", "WebGui"); - // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui"); - // webgui.autoStartBrowser = false; - Runtime.startConfig("webgui"); - Runtime.start("webgui", "WebGui"); + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveLeftHand", map); + } - Random random = (Random) Runtime.start("random", "Random"); + public void openHands() { + openLeftHand(); + openRightHand(); + } - random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + public void closeHands() { + closeLeftHand(); + closeRightHand(); + } + + public Event onEvent(Event event) { + + return event; + } + + public void wake() { + log.info("wake"); + // do waking things - based on config + + // blink + + // wake gesture + // callback + // imoov2[{name}]["onWake"](this) + /** + *
    +     i01.speakBlocking("I was sleeping")
    +     lookrightside()
    +     sleep(2)
    +     lookleftside()
    +     sleep(4)
    +     relax()
    +     ear.clearLock()
    +     sleep(2)
    +     i01.finishedGesture()
    +     * 
    + */ + + /** + *
    +     * // legacy
    +     * enable();
    +     * rest();
    +     * 
    +     * if (ear != null) {
    +     *   ear.clearLock();
    +     * }
    +     * 
    +     * // beginCheckingOnInactivity();
    +     * // BAD BAD BAD !!!
    +     * publishEvent("powerUp"); // before or after loopback
    +     * 
    + **/ + // was a relax gesture .. might want to ask about it .. + + // if ear start listening + AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) getPeer("ear"); + if (ear != null) { + ear.startListening(); + } - random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + // attempt recognize where its at - random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + // attempt to recognize people - random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); - random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); + // look for activity - random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); - random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); + // say hello - random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); - random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); + // start animation (configurable) - random.save(); + rest(); -// i01.startChatBot(); -// -// i01.startAll("COM3", "COM4"); - Runtime.start("python", "Python"); + // should "session be determined by recognition?" + ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); - } catch (Exception e) { - log.error("main threw", e); + if (chatBot != null) { + String firstinit = chatBot.getPredicate("firstinit"); + // wtf - "ok" really, for a boolean? + if (!"ok".equals(firstinit)) { + fsm.fire("firstInit"); + } } } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index 11d71fba7c..a8b3fb2b4f 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -142,15 +142,34 @@ public void disable() { eyelidRight.disable(); } - public long getLastActivityTime() { - - long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); - lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + public Long getLastActivityTime() { + + Long lastActivityTime = Math.max(rothead.getLastActivityTime(), neck.getLastActivityTime()); + if (getPeer("eyeX") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeX.getLastActivityTime()); + } + if (getPeer("eyeY") != null) { + lastActivityTime = Math.max(lastActivityTime, eyeY.getLastActivityTime()); + } + if (getPeer("jaw") != null) { + lastActivityTime = Math.max(lastActivityTime, jaw.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rollNeck.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, rothead.getLastActivityTime()); + } + if (getPeer("rollNeck") != null) { + lastActivityTime = Math.max(lastActivityTime, neck.getLastActivityTime()); + } + + if (getPeer("eyelidLeft") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidLeft.getLastActivityTime()); + } + if (getPeer("eyelidRight") != null) { + lastActivityTime = Math.max(lastActivityTime, eyelidRight.getLastActivityTime()); + } return lastActivityTime; } diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 05524a2630..378fd2440c 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -247,6 +247,12 @@ public void publishTelemetry(String name) { log.info("Servo : {} Angle : {}", jointName, angleMap.get(jointName)); } invoke("publishJointAngles", angleMap); + + InMoov2 i01 = (InMoov2)Runtime.getService("i01"); + if (i01 != null) { + i01.onJointAngles(angleMap); + } + // we want to publish the joint positions // this way we can render on the web gui.. double[][] jointPositionMap = createJointPositionMap(name); @@ -292,8 +298,7 @@ public static void main(String[] args) throws Exception { LoggingFactory.init("info"); String arm = "myArm"; - Runtime.createAndStart("python", "Python"); - Runtime.createAndStart("gui", "SwingGui"); + Runtime.start("python", "Python"); InverseKinematics3D inversekinematics = (InverseKinematics3D) Runtime.start("ik3d", "InverseKinematics3D"); // InverseKinematics3D inversekinematics = new InverseKinematics3D("iksvc"); diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index fbc5ef5e99..2174ac739b 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -1469,7 +1469,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": @@ -2157,7 +2157,7 @@ public void simpleInitApp() { new File(getDataDir()).mkdirs(); new File(getResourceDir()).mkdirs(); - // assetManager.registerLocator("./", FileLocator.class); + assetManager.registerLocator("./", FileLocator.class); assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLocator(assetsDir, FileLocator.class); assetManager.registerLocator(modelsDir, FileLocator.class); diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java index fa88732014..3e1fe48847 100644 --- a/src/main/java/org/myrobotlab/service/Log.java +++ b/src/main/java/org/myrobotlab/service/Log.java @@ -34,7 +34,7 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.Logging; import org.myrobotlab.logging.LoggingFactory; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.config.LogConfig; import org.slf4j.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -44,7 +44,7 @@ import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.status.Status; -public class Log extends Service implements Appender { +public class Log extends Service implements Appender { public static class LogEntry { public long ts; @@ -81,7 +81,7 @@ public String toString() { * broadcast logging is through publishLogEvent (not broadcastState) */ transient List buffer = new ArrayList<>(); - + /** * logging state */ @@ -192,6 +192,7 @@ public void doAppend(ILoggingEvent event) throws LogbackException { synchronized public void flush() { if (buffer.size() > 0) { invoke("publishLogEvents", buffer); + buffer = new ArrayList<>(maxSize); lastPublishLogTimeTs = System.currentTimeMillis(); } @@ -224,6 +225,11 @@ public List publishLogEvents(List entries) { return entries; } + public List publishErrors(List entries) { + return entries; + } + + @Override public void setContext(Context arg0) { // TODO Auto-generated method stub diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e13daf273d..924f7ef219 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -60,6 +60,12 @@ private class AnimationRunner implements Runnable { public void run() { try { running = true; + while (running) { + LedDisplayData led = displayQueue.take(); + // save existing state if necessary .. + // stop animations if running + // String lastAnimation = currentAnimation; + if ((led.count > 0) && (currentAnimation == null)){ while (running) { equalizer(); @@ -430,6 +436,33 @@ public void flash(int count, long interval, int r, int g, int b) { } + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, flashTimeOn, flashTimeOff); + } + + public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData(); + data.red = r; + data.green = g; + data.blue = b; + data.count = count; + data.timeOn = timeOn; + data.timeOff = timeOff; + displayQueue.add(data); + } + + public void onPlayAnimation(String animation) { + playAnimation(animation); + } + + public void onStopAnimation() { + stopAnimation(); + } + + public void onFlash(LedDisplayData data) { + displayQueue.add(data); + } + public void flashBrightness(double brightNess) { NeoPixelConfig c = (NeoPixelConfig)config; @@ -580,6 +613,23 @@ public int getRed() { @Override public void playAnimation(String animation) { + if (animation == null) { + log.info("playAnimation null"); + return; + } + + if (animation.equals(currentAnimation)) { + log.info("already playing {}", currentAnimation); + return; + } + +// if ("Snake".equals(animation)){ +// LedDisplayData snake = new LedDisplayData(); +// snake.red = red; +// snake.green = green; +// snake.blue = blue; +// displayQueue.add(null); +// } else if (animations.containsKey(animation)) { currentAnimation = animation; 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/Pir.java b/src/main/java/org/myrobotlab/service/Pir.java index 5a2f0d750c..eea710f585 100644 --- a/src/main/java/org/myrobotlab/service/Pir.java +++ b/src/main/java/org/myrobotlab/service/Pir.java @@ -9,7 +9,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.PirConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.PinData; import org.myrobotlab.service.interfaces.PinArrayControl; import org.myrobotlab.service.interfaces.PinDefinition; @@ -52,33 +51,31 @@ public void attach(String name) { } public void setPinArrayControl(String control) { - PirConfig c = (PirConfig) config; - c.controller = control; + config.controller = control; } public void attachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { error("controller cannot be null"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin should be set before attaching"); return; } - c.controller = CodecUtils.getShortName(control); + config.controller = CodecUtils.getShortName(control); // fire and forget - send(c.controller, "attach", getName()); + send(config.controller, "attach", getName()); // assume worky isAttached = true; // enable if configured - if (c.enable) { - send(c.controller, "enablePin", c.pin, c.rate); + if (config.enable) { + send(config.controller, "enablePin", config.pin, config.rate); } broadcastState(); @@ -95,16 +92,15 @@ public void detach(String name) { * @param control */ public void detachPinArrayControl(String control) { - PirConfig c = (PirConfig) config; if (control == null) { log.info("detaching null"); return; } - if (c.controller != null) { - if (!c.controller.equals(control)) { - log.warn("attempting to detach {} but this pir is attached to {}", control, c.controller); + if (config.controller != null) { + if (!config.controller.equals(control)) { + log.warn("attempting to detach {} but this pir is attached to {}", control, config.controller); return; } } @@ -112,8 +108,8 @@ public void detachPinArrayControl(String control) { // disable disable(); - send(c.controller, "detach", getName()); - // c.controller = null; left as configuration .. "last controller" + send(config.controller, "detach", getName()); + // config.controller = null; left as configuration .. "last controller" // detached isAttached = false; @@ -128,13 +124,12 @@ public void detachPinArrayControl(String control) { * */ public void disable() { - PirConfig c = (PirConfig) config; - if (c.controller != null && c.pin != null) { - send(c.controller, "disablePin", c.pin); + if (config.controller != null && config.pin != null) { + send(config.controller, "disablePin", config.pin); } - c.enable = false; + config.enable = false; active = null; broadcastState(); } @@ -143,8 +138,7 @@ public void disable() { * Enables polling at the preset poll rate. */ public void enable() { - PirConfig c = (PirConfig) config; - enable(c.rate); + enable(config.rate); } /** @@ -154,14 +148,13 @@ public void enable() { * */ public void enable(int rateHz) { - PirConfig c = (PirConfig) config; - if (c.controller == null) { + if (config.controller == null) { error("pin control not set"); return; } - if (c.pin == null) { + if (config.pin == null) { error("pin not set"); return; } @@ -171,17 +164,16 @@ public void enable(int rateHz) { return; } - c.rate = rateHz; + config.rate = rateHz; /* PinArrayControl.enablePin */ - send(c.controller, "enablePin", c.pin, rateHz); - c.enable = true; + send(config.controller, "enablePin", config.pin, rateHz); + config.enable = true; broadcastState(); } @Override public String getPin() { - PirConfig c = (PirConfig) config; - return c.pin; + return config.pin; } /** @@ -190,8 +182,7 @@ public String getPin() { * @return Hz */ public int getRate() { - PirConfig c = (PirConfig) config; - return c.rate; + return config.rate; } /** @@ -211,8 +202,7 @@ public boolean isActive() { * @return true = Enabled. false = Disabled. */ public boolean isEnabled() { - PirConfig c = (PirConfig) config; - return c.enable; + return config.enable; } @Override @@ -247,13 +237,12 @@ public PirConfig getConfig() { @Override public void onPin(PinData pindata) { - PirConfig c = (PirConfig) config; log.debug("onPin {}", pindata); boolean sense = (pindata.value != 0); // sparse publishing only on state change - if (active == null || active != sense && c.enable) { + if (active == null || active != sense && config.enable) { // state change invoke("publishSense", sense); active = sense; @@ -286,14 +275,12 @@ public void publishPirOff() { */ @Override public void setPin(String pin) { - PirConfig c = (PirConfig) config; - c.pin = pin; + config.pin = pin; } @Deprecated /* use attach(String) */ public void setPinArrayControl(PinArrayControl pinControl) { - PirConfig c = (PirConfig) config; - c.controller = pinControl.getName(); + config.controller = pinControl.getName(); } /** @@ -306,8 +293,7 @@ public void setRate(int rateHz) { error("invalid poll rate - default is 1 Hz valid value is > 0"); return; } - PirConfig c = (PirConfig) config; - c.rate = rateHz; + config.rate = rateHz; } /** diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index 93b8178fd2..dca027aa3a 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -183,14 +183,17 @@ public void addRandom(long minIntervalMs, long maxIntervalMs, String name, Strin msg.interval = getRandom(minIntervalMs, maxIntervalMs); log.info("add random message {} in {} ms", key, msg.interval); - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } broadcastState(); } public void process(String key) { - if (!enabled) { - return; - } + // if (!enabled) { + // return; + // } RandomMessage msg = randomData.get(key); if (msg == null || !msg.enabled) { @@ -230,7 +233,11 @@ public void process(String key) { purgeTask(key); if (!msg.oneShot) { msg.interval = getRandom(msg.minIntervalMs, msg.maxIntervalMs); - addTask(key, 0, msg.interval, "process", key); + // must re-schedule unless one shot + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } } } @@ -316,7 +323,10 @@ public void enable(String key) { return; } randomData.get(key).enabled = true; - addTask(key, 0, msg.interval, "process", key); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(key, 0, msg.interval, "process", key); + } return; } // must be name - disable "all" for this service @@ -325,7 +335,10 @@ public void enable(String key) { if (msg.name.equals(name)) { msg.enabled = true; String fullKey = String.format("%s.%s", msg.name, msg.method); - addTask(fullKey, 0, msg.interval, "process", fullKey); + if (enabled) { + // only if global enabled is enabled do we start the task + addTask(fullKey, 0, msg.interval, "process", fullKey); + } } } } @@ -335,6 +348,7 @@ public void disable() { // events purgeTasks(); enabled = false; + broadcastState(); } public void enable() { @@ -345,7 +359,9 @@ public void enable() { addTask(fullKey, 0, msg.interval, "process", fullKey); } } + enabled = true; + broadcastState(); } public void purge() { @@ -392,9 +408,10 @@ public static void main(String[] args) { List ret = random.getServiceList(); Set mi = random.getMethodsFromName("c1"); List mes = MethodCache.getInstance().query("Clock", "setInterval"); - + random.disable(); random.addRandom(200, 1000, "i01", "setHeadSpeed", 8, 20, 8, 20, 8, 20); random.addRandom(200, 1000, "i01", "moveHead", 65, 115, 65, 115, 65, 115); + random.enable(); // Python python = (Python) Runtime.start("python", "Python"); diff --git a/src/main/java/org/myrobotlab/service/Updater.java b/src/main/java/org/myrobotlab/service/Updater.java index 779dc1c81f..f97f3149e6 100644 --- a/src/main/java/org/myrobotlab/service/Updater.java +++ b/src/main/java/org/myrobotlab/service/Updater.java @@ -4,11 +4,32 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Properties; import java.util.Set; import java.util.TreeSet; +import org.eclipse.jgit.api.PullCommand; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.api.errors.DetachedHeadException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.BranchTrackingStatus; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.CmdOptions; import org.myrobotlab.framework.MrlException; @@ -392,10 +413,13 @@ public void checkForUpdates() { if (isSrcMode) { String cwd = System.getProperty("user.dir"); boolean makeBuild = false; - String branch = Git.getBranch(); + + Repository repo = new FileRepositoryBuilder().setGitDir(new File(System.getProperty("user.dir"))).build(); + org.eclipse.jgit.api.Git git = new org.eclipse.jgit.api.Git(repo); + String branch = git.getRepository().getBranch(); log.info("current source branch is \"{}\"", branch); - int commitsBehind = Git.pull(branch); + int commitsBehind = pull(null, branch); if (gitProps == null) { log.info("target/classes/git.properties does not exist - will build"); @@ -413,7 +437,8 @@ public void checkForUpdates() { // FIXME - download mvn if it does not exist ?? // remove git properties before compile - Git.removeProps(); + File props = new File(System.getProperty("user.dir") + File.separator + "target" + File.separator + "classes" + File.separator + "git.properties"); + props.delete(); // FIXME - compile or package mode ! String ret = Maven.mvn(cwd, branch, "compile", System.currentTimeMillis() / 1000, offline); @@ -613,6 +638,73 @@ public boolean accept(File dir, String name) { return false; } + TextProgressMonitor monitor = new TextProgressMonitor(); + + public int pull(String src, String branch) throws IOException, WrongRepositoryStateException, InvalidConfigurationException, DetachedHeadException, InvalidRemoteException, + CanceledException, RefNotFoundException, NoHeadException, TransportException, GitAPIException { + + if (src == null) { + src = System.getProperty("user.dir"); + } + + if (branch == null) { + log.warn("branch is not set - setting to default develop"); + branch = "develop"; + } + + List branches = new ArrayList(); + branches.add("refs/heads/" + branch); + + File repoParentFolder = new File(src); + + org.eclipse.jgit.api.Git git = null; + Repository repo = null; + + // Open an existing repository FIXME Try Git.open(dir) + String gitDir = repoParentFolder.getAbsolutePath() + "/.git"; + repo = new FileRepositoryBuilder().setGitDir(new File(gitDir)).build(); + git = new org.eclipse.jgit.api.Git(repo); + + repo = git.getRepository(); + git.branchCreate().setForce(true).setName(branch).setStartPoint(branch).call(); + git.checkout().setName(branch).call(); + + git.fetch().setProgressMonitor(monitor).call(); + + List localLogs = getLogs(git, "origin/" + branch, 1); + List remoteLogs = getLogs(git, "remotes/origin/" + branch, 1); + + RevCommit localCommit = localLogs.get(0); + RevCommit remoteCommit = remoteLogs.get(0); + + BranchTrackingStatus status = BranchTrackingStatus.of(repo, branch); + + // FIXME - Git.close() file handles + + if (status.getBehindCount() > 0) { + log.info("local ts {}, remote {} - {} pulling", localCommit.getCommitTime(), remoteCommit.getCommitTime(), remoteCommit.getFullMessage()); + PullCommand pullCmd = git.pull(); + pullCmd.setProgressMonitor(monitor); + pullCmd.call(); + git.close(); + return status.getBehindCount(); + } + log.info("no new commits on branch {}", branch); + git.close(); + return 0; + } + + private List getLogs(org.eclipse.jgit.api.Git git, String ref, int maxCount) + throws RevisionSyntaxException, NoHeadException, MissingObjectException, IncorrectObjectTypeException, AmbiguousObjectException, GitAPIException, IOException { + List ret = new ArrayList<>(); + Repository repository = git.getRepository(); + Iterable logs = git.log().setMaxCount(maxCount).add(repository.resolve(ref)).call(); + for (RevCommit rev : logs) { + ret.add(rev); + } + return ret; + } + public static void main(String[] args) { LoggingFactory.init(Level.INFO); 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..63a3f1f3ed --- /dev/null +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -0,0 +1,155 @@ +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"); + Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + 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/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index ecb2d59c4e..8e07cbf5ee 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -56,7 +56,9 @@ public class InMoov2Config extends ServiceConfig { public boolean openCVFlipPicture=false; public boolean pirEnableTracking = false; - + + public boolean pirOnFlash = true; + /** * play pir sounds when pir switching states * sound located in data/InMoov2/sounds/pir-activated.mp3 @@ -65,10 +67,9 @@ public class InMoov2Config extends ServiceConfig { public boolean pirPlaySounds = true; public boolean pirWakeUp = true; - - public boolean robotCanMoveHeadWhileSpeaking = true; - + public boolean robotCanMoveHeadWhileSpeaking = true; + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest @@ -81,13 +82,45 @@ public class InMoov2Config extends ServiceConfig { public int sleepTimeoutMs=300000; public boolean startupSound = true; - - public int trackingTimeoutMs=10000; + /** + * + */ + public boolean stateChangeIsMute = true; + + /** + * Interval in seconds for a idle state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to idle. Heartbeat will fire the event. + */ + public Integer stateIdleInterval = 120; + + + /** + * Interval in seconds for a random state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to random. Heartbeat will fire the event. + */ + public Integer stateRandomInterval = 120; + + /** + * Determines if InMoov2 publish system events during boot state + */ + public boolean systemEventsOnBoot = false; + + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; + + public int trackingTimeoutMs = 10000; + public String unlockInsult = "forgive me"; public boolean virtual = false; + public String bootAnimation = "Theater Chase"; + public InMoov2Config() { } @@ -112,6 +145,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "left", "Arduino", false); addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false); addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false); + addDefaultPeerConfig(plan, name, "log", "Log", false); addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false); addDefaultPeerConfig(plan, name, "mouthControl", "MouthControl", false); addDefaultPeerConfig(plan, name, "neoPixel", "NeoPixel", false); @@ -256,22 +290,28 @@ public Plan getDefault(Plan plan, String name) { FiniteStateMachineConfig fsm = (FiniteStateMachineConfig) plan.get(getPeerName("fsm")); // TODO - events easily gotten from InMoov data ?? auto callbacks in python if exists ? fsm.current = "boot"; - fsm.transitions.add(new Transition("boot", "configStarted", "applyingConfig")); - fsm.transitions.add(new Transition("applyingConfig", "getUserInfo", "getUserInfo")); - fsm.transitions.add(new Transition("applyingConfig", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("applyingConfig", "wake", "awake")); - fsm.transitions.add(new Transition("getUserInfo", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); - fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); + fsm.transitions.add(new Transition("boot", "wake", "wake")); + fsm.transitions.add(new Transition("wake", "idle", "idle")); + fsm.transitions.add(new Transition("firstInit", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "random", "random")); + fsm.transitions.add(new Transition("random", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "sleep", "sleep")); + fsm.transitions.add(new Transition("sleep", "wake", "wake")); + fsm.transitions.add(new Transition("idle", "powerDown", "powerDown")); + fsm.transitions.add(new Transition("wake", "firstInit", "firstInit")); + // powerDown to shutdown +// fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); +// fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); PirConfig pir = (PirConfig) plan.get(getPeerName("pir")); - pir.pin = "23"; + pir.pin = "D23"; pir.controller = name + ".left"; pir.listeners = new ArrayList<>(); - pir.listeners.add(new Listener("publishPirOn", name, "onPirOn")); - + pir.listeners.add(new Listener("publishPirOn", name)); + pir.listeners.add(new Listener("publishPirOff", name)); + // == Peer - random ============================= RandomConfig random = (RandomConfig) plan.get(getPeerName("random")); random.enabled = false; @@ -388,6 +428,45 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishEvent", name + ".fsm")); + // loopbacks allow user to override or extend with python + listeners.add(new Listener("publishBoot", name)); + listeners.add(new Listener("publishHeartbeat", name)); + listeners.add(new Listener("publishConfigFinished", name)); + listeners.add(new Listener("publishStateChange", name)); + +// listeners.add(new Listener("publishPowerUp", name)); +// listeners.add(new Listener("publishPowerDown", name)); +// listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + // service --to--> InMoov2 + AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); + mouth_audioFile.listeners = new ArrayList<>(); + mouth_audioFile.listeners.add(new Listener("publishPeak", name)); + fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); + + + LogConfig log = (LogConfig) plan.get(getPeerName("log")); + log.listeners = new ArrayList<>(); + log.listeners.add(new Listener("publishLogEvents", name)); + +// mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); +// mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + + // InMoov2 --to--> service + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay")); + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + + listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel"))); + listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel"))); + // remove the auto-added starts in the plan's runtime RuntimConfig.registry plan.removeStartsWith(name + "."); 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..a61bd32a1f --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -0,0 +1,97 @@ +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 + + //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..46df333391 --- /dev/null +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java @@ -0,0 +1,108 @@ +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; + +/** + * 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 event) { + + // ctx.writeTextMessage("ping"); FIXME - query ? + // FIXME - thread-safe ? how many connections mapped to objects ? + event.textMessageHandler(new TextMessageHandler(service)); + + } + +} From cf072b0b0ac8aabb98e885b3f2281e0faf868320 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 20:39:35 -0700 Subject: [PATCH 032/232] last of the merge problems hopefully --- .../java/org/myrobotlab/service/InMoov2.java | 102 ------------------ .../java/org/myrobotlab/service/NeoPixel.java | 29 ----- .../service/config/InMoov2Config.java | 20 +--- 3 files changed, 3 insertions(+), 148 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index dccde82fb8..2227e64cde 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -59,8 +59,6 @@ public class InMoov2 extends Service implements ServiceLifeCycleL static String speechRecognizer = "WebkitSpeechRecognition"; - protected static final Set stateDefaults = new TreeSet<>(); - /** * This method will load a python file into the python interpreter. * @@ -1238,97 +1236,6 @@ public void onMoveLeftHand(Map map) { } } - // public Message publishPython(String method, Object...data) { - // return Message.createMessage(getName(), getName(), method, data); - // } - - public void onMoveRightArm(Map map) { - InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); - if (rightArm != null) { - rightArm.onMove(map); - } - } - - public void onMoveRightHand(Map map) { - InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); - if (rightHand != null) { - rightHand.onMove(map); - } - } - - public void onMoveTorso(Map map) { - InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); - if (torso != null) { - torso.onMove(map); - } - } - - public void onMoveHead(Map map) { - InMoov2Head head = (InMoov2Head) getPeer("head"); - if (head != null) { - head.onMove(map); - } - } - - public void onMoveLeftArm(Map map) { - InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); - if (leftArm != null) { - leftArm.onMove(map); - } - } - - public void onMoveLeftHand(Map map) { - InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); - if (leftHand != null) { - leftHand.onMove(map); - } - } - - // public Message publishPython(String method, Object...data) { - // return Message.createMessage(getName(), getName(), method, data); - // } - - public void onMoveRightArm(Map map) { - InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); - if (rightArm != null) { - rightArm.onMove(map); - } - } - - public void onMoveRightHand(Map map) { - InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); - if (rightHand != null) { - rightHand.onMove(map); - } - } - - public void onMoveTorso(Map map) { - InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); - if (torso != null) { - torso.onMove(map); - } - } - - public void onMoveHead(Map map) { - InMoov2Head head = (InMoov2Head) getPeer("head"); - if (head != null) { - head.onMove(map); - } - } - - public void onMoveLeftArm(Map map) { - InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); - if (leftArm != null) { - leftArm.onMove(map); - } - } - - public void onMoveLeftHand(Map map) { - InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); - if (leftHand != null) { - leftHand.onMove(map); - } - } // public Message publishPython(String method, Object...data) { // return Message.createMessage(getName(), getName(), method, data); @@ -1623,15 +1530,6 @@ public void publishInactivity() { fsm.fire("inactvity"); } - /** - * if inactivityTime configured, this event is published after there has not - * been in activity since. - */ - public void publishInactivity() { - log.info("publishInactivity"); - fsm.fire("inactvity"); - } - /** * A more extensible interface point than publishEvent FIXME - create * interface for this diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 6955dda14f..20db992f20 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -63,13 +63,6 @@ private class Worker implements Runnable { public void run() { try { running = true; - while (running) { - LedDisplayData led = displayQueue.take(); - // save existing state if necessary .. - // stop animations if running - // String lastAnimation = currentAnimation; - if ((led.count > 0) && (currentAnimation == null)){ - while (running) { LedDisplayData led = displayQueue.take(); // save existing state if necessary .. @@ -411,17 +404,6 @@ public void flash(int r, int g, int b) { public void flash(int r, int g, int b, int count) { flash(r, g, b, count, flashTimeOn, flashTimeOff); } - - public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { - LedDisplayData data = new LedDisplayData(); - data.red = r; - data.green = g; - data.blue = b; - data.count = count; - data.timeOn = timeOn; - data.timeOff = timeOff; - displayQueue.add(data); - } public void onPlayAnimation(String animation) { playAnimation(animation); @@ -431,17 +413,6 @@ public void onStopAnimation() { stopAnimation(); } - public void onFlash(LedDisplayData data) { - displayQueue.add(data); - } - - public void flashBrightness(double brightNess) { - NeoPixelConfig c = (NeoPixelConfig)config; - - public void onStopAnimation() { - stopAnimation(); - } - public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { LedDisplayData data = new LedDisplayData(); data.red = r; diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index bc417b174f..ad359a5913 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -122,14 +122,7 @@ public class InMoov2Config extends ServiceConfig { * */ public boolean stateChangeIsMute = true; - - public boolean startupSound = true; - - /** - * - */ - public boolean stateChangeIsMute = true; - + /** * Interval in seconds for a idle state event to fire off. * If the fsm is in a state which will allow transitioning, the InMoov2 @@ -497,12 +490,7 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishMoveLeftHand", name)); listeners.add(new Listener("publishMoveTorso", name)); - // service --to--> InMoov2 - AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); - mouth_audioFile.listeners = new ArrayList<>(); - mouth_audioFile.listeners.add(new Listener("publishPeak", name)); - fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); - + LogConfig log = (LogConfig) plan.get(getPeerName("log")); log.listeners = new ArrayList<>(); @@ -519,9 +507,6 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel"))); listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel"))); - // remove the auto-added starts in the plan's runtime RuntimConfig.registry - plan.removeStartsWith(name + "."); - // listeners.add(new Listener("publishPowerUp", name)); // listeners.add(new Listener("publishPowerDown", name)); // listeners.add(new Listener("publishError", name)); @@ -533,6 +518,7 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishMoveLeftHand", name)); listeners.add(new Listener("publishMoveTorso", name)); + // service --to--> InMoov2 AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); mouth_audioFile.listeners = new ArrayList<>(); From d52aaa22d50648d514af52ee7a301ebb6f536c65 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 21:21:50 -0700 Subject: [PATCH 033/232] config specific --- src/main/java/org/myrobotlab/service/NeoPixel.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 20db992f20..5c875b171b 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -45,7 +45,7 @@ import org.myrobotlab.service.interfaces.NeoPixelController; import org.slf4j.Logger; -public class NeoPixel extends Service implements NeoPixelControl { +public class NeoPixel extends Service implements NeoPixelControl { private BlockingQueue displayQueue = new ArrayBlockingQueue<>(200); @@ -836,7 +836,7 @@ public void playIronman() { } @Override - public ServiceConfig getConfig() { + public NeoPixelConfig getConfig() { NeoPixelConfig config = (NeoPixelConfig) super.getConfig(); // FIXME - remove local fields in favor of config @@ -855,8 +855,8 @@ public ServiceConfig getConfig() { } @Override - public ServiceConfig apply(ServiceConfig c) { - NeoPixelConfig config = (NeoPixelConfig) super.apply(c); + public NeoPixelConfig apply(NeoPixelConfig c) { + super.apply(c); // FIXME - remove local fields in favor of config setPixelDepth(config.pixelDepth); From 4e1a4871d44cd097f0cd8f429399a4e7fc0e6fdc Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 19 Sep 2023 21:24:47 -0700 Subject: [PATCH 034/232] removed cast --- src/main/java/org/myrobotlab/service/NeoPixel.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 5c875b171b..e0726b5921 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -837,8 +837,7 @@ public void playIronman() { @Override public NeoPixelConfig getConfig() { - - NeoPixelConfig config = (NeoPixelConfig) super.getConfig(); + super.getConfig(); // FIXME - remove local fields in favor of config config.pin = pin; config.pixelCount = pixelCount; From 5b442bd8ca72d2ea9694362669a662561dc36c2c Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 08:00:17 -0700 Subject: [PATCH 035/232] quiet maven tests --- Jenkinsfile | 4 +- .../myrobotlab/framework/repo/IvyWrapper.java | 76 ++++++++++++------- .../service/FiniteStateMachine.java | 2 +- .../java/org/myrobotlab/service/NeoPixel.java | 65 +++++++--------- 4 files changed, 80 insertions(+), 67 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 85c66205f7..aad53da63f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -101,11 +101,11 @@ pipeline { // TODO - integration tests ! if (isUnix()) { sh ''' - mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast + mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast -q ''' } else { bat ''' - mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast + mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast -q ''' } } diff --git a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java index 5759c4866e..1608959aba 100644 --- a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java @@ -74,6 +74,7 @@ public void rawlog(String msg, int level) { static String ivysettingsXmlTemplate = null; static String ivyXmlTemplate = null; + transient static IvyWrapper localInstance = null; public static final Filter NO_FILTER = NoFilter.INSTANCE; @@ -436,8 +437,24 @@ synchronized public void install(String location, String[] serviceTypes) { Platform platform = Platform.getLocalInstance(); - // templates [originalname](-[classifier])(-[revision]).[ext] parens are "optional" - String[] cmd = new String[] { "-settings", location + "/ivysettings.xml", "-ivy", location + "/ivy.xml", "-retrieve", location + "/jar" + "/[originalname].[ext]" }; + // templates [originalname](-[classifier])(-[revision]).[ext] parens are + // "optional" + + List cmd = new ArrayList<>(); + cmd.add("-settings"); + cmd.add(location + "/ivysettings.xml"); + cmd.add("-ivy"); + cmd.add(location + "/ivy.xml"); + cmd.add("-retrieve"); + cmd.add(location + "/jar" + "/[originalname].[ext]"); + + int msgLevel = 1; + if (log.isWarnEnabled() || log.isErrorEnabled()) { + msgLevel = Message.MSG_WARN; + cmd.add("-warn"); + } else { + msgLevel = Message.MSG_INFO; + } StringBuilder sb = new StringBuilder(); sb.append("wget https://repo1.maven.org/maven2/org/apache/ivy/ivy/" + IVY_VERSION + "/ivy-" + IVY_VERSION + ".jar\n"); @@ -453,14 +470,14 @@ synchronized public void install(String location, String[] serviceTypes) { FileIO.toFile("libraries/install.sh", sb.toString().getBytes()); Ivy ivy = Ivy.newInstance(); // <-- for future 2.5.x release - ivy.getLoggerEngine().pushLogger(new IvyWrapperLogger(Message.MSG_INFO)); + ivy.getLoggerEngine().pushLogger(new IvyWrapperLogger(msgLevel)); ResolveReport report = null; List err = new ArrayList<>(); try { - report = Main.run(cmd); - } catch(Exception e) { - err.add(e.toString()); + report = Main.run(cmd.toArray(new String[cmd.size()])); + } catch (Exception e) { + err.add(e.toString()); } // if no errors -h @@ -490,30 +507,37 @@ synchronized public void install(String location, String[] serviceTypes) { save(); } - ArtifactDownloadReport[] artifacts = report.getAllArtifactsReports(); - for (int i = 0; i < artifacts.length; ++i) { - ArtifactDownloadReport ar = artifacts[i]; - Artifact artifact = ar.getArtifact(); - // String filename = IvyPatternHelper.substitute("[originalname].[ext]", - // artifact); - - File file = ar.getLocalFile(); - String filename = file.getAbsoluteFile().getAbsolutePath(); - log.info("{}", filename); + if (report == null) { + log.error("problems resolving dependencies"); + publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.ERROR, Repo.INSTALL_FINISHED, + String.format("there was problems resolving dependencies %s", (Object[]) serviceTypes))); + } else { - if ("zip".equalsIgnoreCase(artifact.getExt())) { - info("unzipping %s", filename); - try { - Zip.unzip(filename, "./"); - info("unzipped %s", filename); - } catch (Exception e) { - log.error("unable to unzip file {}", filename, e); + ArtifactDownloadReport[] artifacts = report.getAllArtifactsReports(); + for (int i = 0; i < artifacts.length; ++i) { + ArtifactDownloadReport ar = artifacts[i]; + Artifact artifact = ar.getArtifact(); + // String filename = + // IvyPatternHelper.substitute("[originalname].[ext]", + // artifact); + + File file = ar.getLocalFile(); + String filename = file.getAbsoluteFile().getAbsolutePath(); + log.info("{}", filename); + + if ("zip".equalsIgnoreCase(artifact.getExt())) { + info("unzipping %s", filename); + try { + Zip.unzip(filename, "./"); + info("unzipped %s", filename); + } catch (Exception e) { + log.error("unable to unzip file {}", filename, e); + } } } - } - - publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of %s", (Object[]) serviceTypes))); + publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of %s", (Object[]) serviceTypes))); + } } catch (Exception e) { error(e.getMessage()); log.error(e.getMessage(), e); diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index 221fcfcffa..d04436f1e3 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -243,7 +243,7 @@ public List getTransitions() { * @return */ public StateChange publishStateChange(StateChange stateChange) { - log.error("publishStateChange {}", stateChange); + log.info("publishStateChange {}", stateChange); for (String listener : messageListeners) { ServiceInterface service = Runtime.getService(listener); if (service != null) { diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e0726b5921..8e7b84eafa 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -39,7 +39,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.NeoPixelConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.interfaces.NeoPixelControl; import org.myrobotlab.service.interfaces.NeoPixelController; @@ -47,7 +46,7 @@ public class NeoPixel extends Service implements NeoPixelControl { - private BlockingQueue displayQueue = new ArrayBlockingQueue<>(200); + private BlockingQueue displayQueue = new ArrayBlockingQueue<>(200); /** * Thread to do animations Java side and push the changing of pixels to the @@ -388,8 +387,8 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { public void onLedDisplay(LedDisplayData data) { try { displayQueue.add(data); - } catch(IllegalStateException e) { - log.info("queue full"); + } catch (IllegalStateException e) { + log.info("queue full"); } } @@ -398,13 +397,13 @@ public void flash() { } public void flash(int r, int g, int b) { - flash(r, g, b, flashCount , flashTimeOn, flashTimeOff); + flash(r, g, b, flashCount, flashTimeOn, flashTimeOff); } - + public void flash(int r, int g, int b, int count) { flash(r, g, b, count, flashTimeOn, flashTimeOff); } - + public void onPlayAnimation(String animation) { playAnimation(animation); } @@ -423,21 +422,19 @@ public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { data.timeOff = timeOff; displayQueue.add(data); } - + public void onFlash(LedDisplayData data) { displayQueue.add(data); } public void flashBrightness(double brightNess) { - NeoPixelConfig c = (NeoPixelConfig) config; - setBrightness((int) brightNess); fill(red, green, blue); - if (c.autoClear) { + if (config.autoClear) { purgeTask("clear"); // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + addTaskOneShot(config.idleTimeout, "clear"); } } @@ -451,8 +448,6 @@ public void fill(int beginAddress, int count, int r, int g, int b) { } public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { - NeoPixelConfig c = (NeoPixelConfig) config; - if (w == null) { w = 0; } @@ -464,10 +459,10 @@ public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { } np2.neoPixelFill(getName(), beginAddress, count, r, g, b, w); - if (c.autoClear) { + if (config.autoClear) { purgeTask("clear"); // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + addTaskOneShot(config.idleTimeout, "clear"); } } @@ -570,19 +565,19 @@ public void playAnimation(String animation) { log.info("playAnimation null"); return; } - + if (animation.equals(currentAnimation)) { log.info("already playing {}", currentAnimation); return; } - -// if ("Snake".equals(animation)){ -// LedDisplayData snake = new LedDisplayData(); -// snake.red = red; -// snake.green = green; -// snake.blue = blue; -// displayQueue.add(null); -// } else + + // if ("Snake".equals(animation)){ + // LedDisplayData snake = new LedDisplayData(); + // snake.red = red; + // snake.green = green; + // snake.blue = blue; + // displayQueue.add(null); + // } else if (animations.containsKey(animation)) { currentAnimation = animation; @@ -649,8 +644,6 @@ public void setBlue(int blue) { } public void setBrightness(int value) { - NeoPixelConfig c = (NeoPixelConfig) config; - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); if (controller == null || np2 == null) { error("%s cannot setPixel controller not set", getName()); @@ -659,10 +652,10 @@ public void setBrightness(int value) { brightness = value; np2.neoPixelSetBrightness(getName(), value); - if (c.autoClear) { + if (config.autoClear) { purgeTask("clear"); // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + addTaskOneShot(config.idleTimeout, "clear"); } } @@ -714,7 +707,6 @@ public void setPixel(int address, int red, int green, int blue, int white) { * @param delayMs */ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int red, int green, int blue, int white, Integer delayMs) { - NeoPixelConfig c = (NeoPixelConfig) config; // get and update memory cache PixelSet ps = getPixelSet(matrixName, pixelSetIndex); @@ -743,10 +735,10 @@ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int np2.neoPixelWriteMatrix(getName(), pixel.flatten()); - if (c.autoClear) { + if (config.autoClear) { purgeTask("clear"); // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + addTaskOneShot(config.idleTimeout, "clear"); } } @@ -796,18 +788,16 @@ public void setColor(int red, int green, int blue) { @Override public void writeMatrix() { - NeoPixelConfig c = (NeoPixelConfig) config; - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); if (controller == null || np2 == null) { error("%s cannot writeMatrix controller not set", getName()); return; } np2.neoPixelWriteMatrix(getName(), getPixelSet().flatten()); - if (c.autoClear) { + if (config.autoClear) { purgeTask("clear"); // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + addTaskOneShot(config.idleTimeout, "clear"); } } @@ -986,8 +976,7 @@ public void setPin(String pin) { } public boolean setAutoClear(boolean b) { - NeoPixelConfig c = (NeoPixelConfig) config; - c.autoClear = b; + config.autoClear = b; return b; } From 2e567a264724538342275128abd3ad3b84ef80f0 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 08:01:17 -0700 Subject: [PATCH 036/232] quiet gitactions tests --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf668124e0..0a51dd635c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,4 +16,4 @@ jobs: java-version: '11' distribution: 'adopt' - name: Build with Maven - run: mvn --batch-mode -Dtest=!**/OpenCV* test -X + run: mvn --batch-mode -Dtest=!**/OpenCV* test -q From 167498fb5c757c9991f8269679582379276cf41c Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 08:38:51 -0700 Subject: [PATCH 037/232] updated dependency error will throw --- .../myrobotlab/framework/repo/IvyWrapper.java | 18 ++++++++++-------- .../org/myrobotlab/framework/repo/Repo.java | 16 ++++++++-------- .../java/org/myrobotlab/service/Runtime.java | 3 ++- .../myrobotlab/framework/repo/RepoTest.java | 6 ++---- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java index 1608959aba..8f124ba52a 100644 --- a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java @@ -405,9 +405,9 @@ public void installDependency(String location, ServiceDependency library) { } @Override - synchronized public void install(String location, String[] serviceTypes) { + synchronized public void install(String location, String[] serviceTypes) throws IOException { - try { + // try { Set targetLibraries = getUnfulfilledDependencies(serviceTypes); @@ -508,9 +508,11 @@ synchronized public void install(String location, String[] serviceTypes) { } if (report == null) { - log.error("problems resolving dependencies"); + String errorDetail = String.format("there was problems resolving dependencies %s", (Object[]) serviceTypes); + log.error(errorDetail); publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.ERROR, Repo.INSTALL_FINISHED, - String.format("there was problems resolving dependencies %s", (Object[]) serviceTypes))); + errorDetail)); + throw new RuntimeException(errorDetail); } else { ArtifactDownloadReport[] artifacts = report.getAllArtifactsReports(); @@ -538,10 +540,10 @@ synchronized public void install(String location, String[] serviceTypes) { publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of %s", (Object[]) serviceTypes))); } - } catch (Exception e) { - error(e.getMessage()); - log.error(e.getMessage(), e); - } +// } catch (Exception e) { +// error(e.getMessage()); +// log.error(e.getMessage(), e); +// } } diff --git a/src/main/java/org/myrobotlab/framework/repo/Repo.java b/src/main/java/org/myrobotlab/framework/repo/Repo.java index 2253e4753f..594ff0931a 100644 --- a/src/main/java/org/myrobotlab/framework/repo/Repo.java +++ b/src/main/java/org/myrobotlab/framework/repo/Repo.java @@ -360,7 +360,7 @@ static public void info(String format, Object... args) { publishStatus(Status.info(format, args)); } - synchronized public void install() { + synchronized public void install() throws Exception { // if a runtime exits we'll broadcast we are starting to install ServiceData sd = ServiceData.getLocalInstance(); info("starting installation of %s services", sd.getServiceTypeNames().length); @@ -368,11 +368,11 @@ synchronized public void install() { info("finished installing %d services", sd.getServiceTypeNames().length); } - synchronized public void install(String serviceType) { + synchronized public void install(String serviceType) throws Exception { install(getInstallDir(), serviceType); } - synchronized public void install(String location, String serviceType) { + synchronized public void install(String location, String serviceType) throws Exception { String[] types = null; if (serviceType == null) { @@ -403,18 +403,18 @@ synchronized public void installDependency(String libraries, String[] installDep abstract public void installDependency(String location, ServiceDependency serviceTypes); - abstract public void install(String location, String[] serviceTypes); + abstract public void install(String location, String[] serviceTypes) throws Exception; - synchronized public void install(String[] serviceTypes) { + synchronized public void install(String[] serviceTypes) throws Exception { install(getInstallDir(), serviceTypes); } - public void installEach() { + public void installEach() throws Exception { String workDir = String.format(String.format("libraries.ivy.services.%d", System.currentTimeMillis())); installEachTo(workDir); } - public void installEachTo(String location) { + public void installEachTo(String location) throws Exception { // if a runtime exits we'll broadcast we are starting to install ServiceData sd = ServiceData.getLocalInstance(); String[] serviceNames = sd.getServiceTypeNames(); @@ -423,7 +423,7 @@ public void installEachTo(String location) { info("finished installing %d services", sd.getServiceTypeNames().length); } - public void installTo(String location) { + public void installTo(String location) throws Exception { // if a runtime exits we'll broadcast we are starting to install ServiceData sd = ServiceData.getLocalInstance(); info("starting installation of %s services", sd.getServiceTypeNames().length); diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index a6d758f489..7f4f9f656e 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -1536,7 +1536,8 @@ public void run() { r.getRepo().install(serviceType); } } catch (Exception e) { - r.error(e); + r.error("dependencies failed - install error", e); + throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage())); } } }; diff --git a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java index 17a7a63c65..f2715dc90d 100644 --- a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java +++ b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java @@ -4,8 +4,6 @@ import static org.junit.Assert.assertTrue; import java.io.File; -import java.io.IOException; -import java.text.ParseException; import java.util.ArrayList; import java.util.Set; @@ -50,7 +48,7 @@ public void setUp() throws Exception { } @Test - public void testAddStatusListener() throws ParseException, IOException { + public void testAddStatusListener() throws Exception { Repo repo = Repo.getInstance(); repo.addStatusPublisher(this); repo.install("Arduino"); @@ -81,7 +79,7 @@ public void testGetUnfulfilledDependencies() { } @Test - public void testIsInstalled() throws ParseException, IOException { + public void testIsInstalled() throws Exception { Repo repo = Repo.getInstance(); repo.clear(); repo.install("Arduino"); From 2acc34f7b0c551ac57d0475b070f0fe83ab2e7a7 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 09:08:36 -0700 Subject: [PATCH 038/232] added --fail-fast to gitactions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a51dd635c..60e40fb403 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,4 +16,4 @@ jobs: java-version: '11' distribution: 'adopt' - name: Build with Maven - run: mvn --batch-mode -Dtest=!**/OpenCV* test -q + run: mvn --batch-mode --fail-fast -Dtest=!**/OpenCV* test -q From 7e1a22dfef6ec150bbc22bd784faf8158aeb5014 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 09:46:14 -0700 Subject: [PATCH 039/232] fast-fail only --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index aad53da63f..6a9d03f05a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -101,11 +101,11 @@ pipeline { // TODO - integration tests ! if (isUnix()) { sh ''' - mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast -q + mvn -Dfile.encoding=UTF-8 -DargLine="-Xmx1024m" verify --fail-fast -q ''' } else { bat ''' - mvn -Dfile.encoding=UTF-8 -Dsurefire.skipAfterFailureCount=1 -DargLine="-Xmx1024m" verify --fail-fast -q + mvn -Dfile.encoding=UTF-8 -DargLine="-Xmx1024m" verify --fail-fast -q ''' } } From c2b4f984698adf3ec42d085423f9168bc618896d Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 11:06:46 -0700 Subject: [PATCH 040/232] out smart goofy maven --- .github/workflows/build.yml | 4 ++- .../myrobotlab/framework/DependencyTest.java | 13 ++++++++ .../org/myrobotlab/service/OpenCVTest.java | 31 ------------------- 3 files changed, 16 insertions(+), 32 deletions(-) create mode 100644 src/test/java/org/myrobotlab/framework/DependencyTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60e40fb403..4a6324d3d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,5 +15,7 @@ jobs: with: java-version: '11' distribution: 'adopt' + - name: Dependency Test + run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest - name: Build with Maven - run: mvn --batch-mode --fail-fast -Dtest=!**/OpenCV* test -q + run: mvn --batch-mode -Dtest=!**/OpenCV* test -q diff --git a/src/test/java/org/myrobotlab/framework/DependencyTest.java b/src/test/java/org/myrobotlab/framework/DependencyTest.java new file mode 100644 index 0000000000..4d264d9852 --- /dev/null +++ b/src/test/java/org/myrobotlab/framework/DependencyTest.java @@ -0,0 +1,13 @@ +package org.myrobotlab.framework; + +import org.junit.Test; +import org.myrobotlab.test.AbstractTest; + +public class DependencyTest extends AbstractTest { + + @Test + public void test() { + System.out.println("dependencies were successful !"); + } + +} 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 From 6b3e1af04bc7336afedcec511b15d2def77c7812 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 11:09:27 -0700 Subject: [PATCH 041/232] make it quiet --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a6324d3d9..93d82942aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,6 @@ jobs: java-version: '11' distribution: 'adopt' - name: Dependency Test - run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest + run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q - name: Build with Maven run: mvn --batch-mode -Dtest=!**/OpenCV* test -q From 801daefd9fa35d065e00b868689c018f6de33e1d Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 14:07:03 -0700 Subject: [PATCH 042/232] fast fail on dependencies for jenkins --- Jenkinsfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 6a9d03f05a..a0e8bed48e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -92,6 +92,26 @@ pipeline { } } // stage compile + stage('dependencies') { + when { + expression { params.verify == 'true' } + } + steps { + script { + // TODO - integration tests ! + if (isUnix()) { + sh ''' + mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q + ''' + } else { + bat ''' + mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q + ''' + } + } + } + } // stage verify + stage('verify') { when { expression { params.verify == 'true' } From 17734b12620d1c46447a40fb256809d685299621 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 14:47:06 -0700 Subject: [PATCH 043/232] update caliko meta --- src/main/java/org/myrobotlab/service/meta/CalikoMeta.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java index 61c6351aac..5e98d56744 100644 --- a/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/CalikoMeta.java @@ -27,7 +27,7 @@ public CalikoMeta() { // for the ui addDependency("au.edu.federation.caliko.visualisation", "caliko-visualisation", "1.3.8"); - addDependency("au.edu.federation.caliko.visualisation", "caliko-demo", "1.3.8"); + addDependency("au.edu.federation.caliko.demo", "caliko-demo", "1.3.8"); // add it to one or many categories addCategory("ik", "inverse kinematics"); From 7f6fa09ec20b4a6894dd15c67715300312014d9a Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 20 Sep 2023 19:57:08 -0700 Subject: [PATCH 044/232] added time for thread to start --- src/test/java/org/myrobotlab/service/ClockTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/myrobotlab/service/ClockTest.java b/src/test/java/org/myrobotlab/service/ClockTest.java index b9761ac743..ad086ff345 100644 --- a/src/test/java/org/myrobotlab/service/ClockTest.java +++ b/src/test/java/org/myrobotlab/service/ClockTest.java @@ -37,7 +37,7 @@ public void testService() throws Exception { assertEquals(interval, clock.getInterval()); clock.startClock(); - Service.sleep(10); + Service.sleep(100); assertTrue(clock.isClockRunning()); clock.stopClock(); From deb94c11a4ffa69a9f3587c301ad31d83605c8de Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 26 Sep 2023 09:52:30 -0700 Subject: [PATCH 045/232] removign "hack" --- .../java/org/myrobotlab/service/InverseKinematics3D.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java index 378fd2440c..48e5ab9a86 100644 --- a/src/main/java/org/myrobotlab/service/InverseKinematics3D.java +++ b/src/main/java/org/myrobotlab/service/InverseKinematics3D.java @@ -247,11 +247,7 @@ public void publishTelemetry(String name) { log.info("Servo : {} Angle : {}", jointName, angleMap.get(jointName)); } invoke("publishJointAngles", angleMap); - - InMoov2 i01 = (InMoov2)Runtime.getService("i01"); - if (i01 != null) { - i01.onJointAngles(angleMap); - } + // we want to publish the joint positions // this way we can render on the web gui.. From b38784c3bde402ac6ff15a6eb81a1a56799619f1 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 26 Sep 2023 10:58:16 -0700 Subject: [PATCH 046/232] LogConfig removed copypasta --- src/main/java/org/myrobotlab/service/config/LogConfig.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/config/LogConfig.java b/src/main/java/org/myrobotlab/service/config/LogConfig.java index 09f74a9481..4caac3e4a5 100644 --- a/src/main/java/org/myrobotlab/service/config/LogConfig.java +++ b/src/main/java/org/myrobotlab/service/config/LogConfig.java @@ -2,8 +2,6 @@ public class LogConfig extends ServiceConfig { - public String currentUserName; - /** * level of log error, warn, info, debug */ From 5f00884facd29010a101acf138e3914c4ea84de1 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 26 Sep 2023 11:41:59 -0700 Subject: [PATCH 047/232] fixes concurrecy issues --- src/main/java/org/myrobotlab/service/Runtime.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index 7f4f9f656e..7d02b26d01 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -139,7 +139,7 @@ public class Runtime extends Service implements MessageListener, * all these requests. */ @Deprecated /* use the filesystem only no memory plan */ - transient final Plan masterPlan = new Plan("runtime"); + transient Plan masterPlan = new Plan("runtime"); /** * thread for non-blocking install of services @@ -4293,7 +4293,8 @@ public Plan getLocalPlan() { */ static public void clearPlan() { Runtime runtime = Runtime.getInstance(); - runtime.masterPlan.clear(); + // fixes concurrent modification + runtime.masterPlan = new Plan("runtime"); runtime.masterPlan.put("runtime", new RuntimeConfig()); // unset config path runtime.configName = null; From 8d9ecdff4b5d55816ad47e347674d63d96a28a6a Mon Sep 17 00:00:00 2001 From: grog Date: Fri, 29 Sep 2023 05:45:04 -0700 Subject: [PATCH 048/232] worky vertx --- .../org/myrobotlab/service/JMonkeyEngine.java | 7 +- .../java/org/myrobotlab/service/Vertx.java | 74 ++++++++++++++----- .../service/config/VertXConfig.java | 17 +++++ .../myrobotlab/service/meta/WebXRMeta.java | 2 +- .../org/myrobotlab/vertx/ApiVerticle.java | 3 +- .../myrobotlab/vertx/WebSocketHandler.java | 46 ++++++++---- 6 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/myrobotlab/service/config/VertXConfig.java diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index 2174ac739b..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 diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java index f87e9b9e4c..8665793d37 100644 --- a/src/main/java/org/myrobotlab/service/Vertx.java +++ b/src/main/java/org/myrobotlab/service/Vertx.java @@ -1,16 +1,24 @@ package org.myrobotlab.service; -import java.util.HashMap; +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. @@ -21,10 +29,10 @@ * * @see https://medium.com/@pvub/https-medium-com-pvub-vert-x-workers-6a8df9b2b9ee * - * @author greg + * @author GroG * */ -public class Vertx extends Service { +public class Vertx extends Service implements Gateway { private static final long serialVersionUID = 1L; @@ -78,9 +86,8 @@ public void stopService() { stop(); } - /** - * + * Undeploy the verticle serving http and ws */ public void stop() { log.info("stopping driver"); @@ -88,27 +95,14 @@ public void stop() { for (String id : ids) { vertx.undeploy(id, (result) -> { if (result.succeeded()) { - log.info("succeeded"); + log.info("undeploy succeeded"); } else { - log.error("failed"); + log.error("undeploy 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 { @@ -132,4 +126,44 @@ public static void main(String[] args) { 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()); + 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); } } 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..6369ef70b5 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/config/VertXConfig.java @@ -0,0 +1,17 @@ +package org.myrobotlab.service.config; + +import java.util.ArrayList; +import java.util.List; + +public class VertXConfig extends ServiceConfig { + + public Integer port = 8443; + public boolean autoStartBrowser = true; + public List resources = new ArrayList<>(); + + public VertXConfig() { + // robot-x-app build directory + // resources.add("./resource/WebGui/app"); + } + +} diff --git a/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java b/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java index 171959fbf5..36efe701ad 100644 --- a/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/WebXRMeta.java @@ -16,7 +16,7 @@ public class WebXRMeta extends MetaData { public WebXRMeta() { // add a cool description - addDescription("WebXr allows hmi devices to add input and get data back from mrl"); + 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); diff --git a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java index a61bd32a1f..57239bd210 100644 --- a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -44,7 +44,8 @@ public void start() throws Exception { router.route().handler(CorsHandler.create("*").allowedMethod(HttpMethod.GET).allowedMethod(HttpMethod.OPTIONS).allowedHeader("Accept").allowedHeader("Authorization") .allowedHeader("Content-Type")); - // static file routing + // 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" //StaticHandler root = StaticHandler.create("src/main/resources/resource/Vertx/app"); // StaticHandler root = StaticHandler.create("src/main/resources/resource/Vertx/app"); diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java index 46df333391..69b56957f3 100644 --- a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java @@ -6,6 +6,7 @@ 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; @@ -15,29 +16,30 @@ /** * Minimal Handler for all websocket messages coming from the react js client. * - * TODO - what else besides text messages - websocket binary streams ??? text stream ? + * 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; } @@ -79,7 +81,7 @@ public void handle(String json) { // } else { // ctx.writeTextMessage("ping"); Useful is writing back // } - + // replace with typed parameters msg.data = params; // queue the message @@ -88,21 +90,37 @@ public void handle(String json) { } 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) { + @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 ? - event.textMessageHandler(new TextMessageHandler(service)); - + socket.textMessageHandler(textMessageHandler); + log.info("new ws connection {}", uuid); + + socket.closeHandler(close -> { + log.info("closing {}", socket.binaryHandlerID()); + Runtime.getInstance().removeConnection(socket.binaryHandlerID()); + }); + } } From ecd64cbacd99342ea48b6b72219430755ed5bf24 Mon Sep 17 00:00:00 2001 From: kwatters Date: Wed, 11 Oct 2023 17:44:58 -0400 Subject: [PATCH 049/232] cleanup yolo filter, don't attach the non-serializable frame to the classification object published. --- .../myrobotlab/document/Classification.java | 8 -- .../myrobotlab/opencv/OpenCVFilterYolo.java | 131 +++++------------- 2 files changed, 34 insertions(+), 105 deletions(-) diff --git a/src/main/java/org/myrobotlab/document/Classification.java b/src/main/java/org/myrobotlab/document/Classification.java index 2f615865a5..727bbd36e0 100644 --- a/src/main/java/org/myrobotlab/document/Classification.java +++ b/src/main/java/org/myrobotlab/document/Classification.java @@ -110,14 +110,6 @@ public BufferedImage getImage() { return null; } - public void setObject(Object frame) { - setField("imageObject", frame); - } - - public Object getObject() { - return getValue("imageObject"); - } - public void setBoundingBox(Rectangle rect) { setField("bounding_box", rect); } diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java index a03c9b20ce..4e409326b7 100755 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java @@ -26,53 +26,41 @@ import org.bytedeco.opencv.opencv_dnn.Net; import org.myrobotlab.document.Classification; import org.myrobotlab.framework.Service; +import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.math.geometry.Rectangle; +import org.myrobotlab.service.OpenCV; import org.slf4j.Logger; +/** + * This filter uses the Yolo image recognition libraries. + * For more information about yolo, here's a link: + * https://pjreddie.com/darknet/yolo/ + * + */ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { private static final long serialVersionUID = 1L; - - public transient final static Logger log = LoggerFactory.getLogger(OpenCVFilterYolo.class); - - protected Boolean running; - - // zero offset to where the confidence level is in the output matrix of the - // darknet. + public final static Logger log = LoggerFactory.getLogger(OpenCVFilterYolo.class); + volatile protected Boolean running; + // offset to where the confidence level is in the output matrix of the darknet. private static final int CONFIDENCE_INDEX = 4; - transient private final OpenCVFrameConverter.ToIplImage grabberConverter = new OpenCVFrameConverter.ToIplImage(); - - protected float confidenceThreshold = 0.25F; - // the column in the detection matrix that contains the confidence level. (I - // think?) - // int probability_index = 5; - // yolo file locations - // private String darknetHome = "c:/dev/workspace/darknet/"; - - // *** the 'correct' way *** - // public String darknetHome = - // FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); - public String darknetHome = "resource/OpenCV/yolo"; // FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); + private float confidenceThreshold = 0.25F; + public String darknetHome = FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); public String modelConfig = "yolov2.cfg"; public String modelWeights = "yolov2.weights"; public String modelNames = "coco.names"; - - int classifierThreadCount = 0; - transient DecimalFormat df2 = new DecimalFormat("#.###"); - transient private OpenCVFrameConverter.ToIplImage converterToIpl = new OpenCVFrameConverter.ToIplImage(); - - protected boolean debug = false; + boolean debug = false; transient private Net net; ArrayList classNames; - // quick fix for exploding serialization of Classification - transient public ArrayList lastResult = null; + public ArrayList lastResult = null; transient private volatile IplImage lastImage = null; private volatile boolean pending = false; transient private Thread classifier; + volatile Object lock = new Object(); public OpenCVFilterYolo(String name) { super(name); @@ -84,11 +72,9 @@ public OpenCVFilterYolo() { } private void loadYolo() { - log.info("loadYolo - begin"); - + log.info("Loading Yolo model."); try { net = readNetFromDarknet(darknetHome + File.separator + modelConfig, darknetHome + File.separator + modelWeights); - log.info("Loaded yolo darknet model to opencv"); } catch (Exception e) { log.error("readNetFromDarknet could not read", e); return; @@ -100,12 +86,10 @@ private void loadYolo() { log.warn("Error unable to load class names from file {}", modelNames, e); return; } - log.info("Done loading model.."); - log.info("loadYolo - end"); + log.info("Yolo model loaded."); } private ArrayList loadClassNames(String filename) throws IOException { - log.info("loadClassNames - begin"); ArrayList names = new ArrayList(); FileReader fileReader = new FileReader(filename); BufferedReader bufferedReader = new BufferedReader(fileReader); @@ -115,18 +99,16 @@ private ArrayList loadClassNames(String filename) throws IOException { names.add(line.trim()); i++; } - log.info("read {} names", i); + log.info("read {} class names", i); fileReader.close(); - - log.info("loadClassNames - end"); return names; } @Override public IplImage process(IplImage image) throws InterruptedException { + // TODO: what is this doing here? if (lastResult != null) { - // the thread running will be updating lastResult for it as fast as it - // can. + // the thread running will be updating lastResult for it as fast as itcan. // displayResult(image, lastResult); } // ok now we just need to update the image that the current thread is @@ -155,29 +137,25 @@ public void run() { running = true; // loading the model takes a lot of time, we want to block enable/disable // until we are actually running - then we notifyAll - while (running && enabled) { if (!pending) { - log.debug("Skipping frame"); + // avoid spinning the cpu too hard. Thread.sleep(10); continue; } - - log.info("process - begin"); + log.info("Yolo Image Frame - Begin"); // only classify this if we haven't already classified it. if (lastImage != null) { - // lastResult = dl4j.classifyImageVGG16(lastImage); - log.debug("Doing yolo..."); lastResult = yoloFrame(lastImage); - // log.info("Yolo done."); - // we processed, next object we'll pick up. + // update this so we will process the next frame that arrives. pending = false; count++; if (count % 10 == 0) { double rate = 1000.0 * count / (System.currentTimeMillis() - start); log.info("Yolo Classification Rate : {}", rate); } - + // TODO: why don't we just publish the lastResult object instead? + // This seems silly.. and potentially this looses data? Map> ret = new TreeMap<>(); for (Classification c : lastResult) { List nl = null; @@ -189,17 +167,12 @@ public void run() { } nl.add(c); } - invoke("publishClassification", ret); } else { - log.info("No Image to classify..."); + // We shouldn't see this? + log.info("Waiting for a frame to process."); } - // TODO: see why there's a race condition. i seem to need a little delay - // here o/w the recognition never seems to start. - // maybe lastImage needs to be marked as volatile ? - - Thread.sleep(1); - } // while (running) + } } catch (Exception e) { log.error("yolo thread threw", e); @@ -208,38 +181,23 @@ public void run() { synchronized (lock) { classifier = null; } - + log.info("yolo exiting classifier thread"); - log.info("run - end"); } private ArrayList yoloFrame(IplImage frame) { - log.debug("Starting yolo on frame..."); - log.info("yoloFrame - begin"); // this is our list of objects that have been detected in a given frame. ArrayList yoloObjects = new ArrayList(); // convert that frame to a matrix (Mat) using the frame converters in javacv - - log.info("yoloFrame - grabberConverter {}", frame); - // log.info("Yolo frame start"); Mat inputMat = grabberConverter.convertToMat(grabberConverter.convert(frame)); - // log.info("Input mat created"); // TODO: I think yolo expects RGB color (which is inverted in the next step) // so if the input image isn't in RGB color, we might need a cvCutColor - log.info("yoloFrame - blobFromImage"); Mat inputBlob = blobFromImage(inputMat, 1 / 255.F, new Size(416, 416), new Scalar(), true, false, CV_32F); // put our frame/input blob into the model. - // log.info("input blob created"); - log.info("yoloFrame - blob {}", inputBlob); net.setInput(inputBlob); - - log.debug("Feed forward!"); - // log.info("Input blob set on network."); // ask for the detection_out layer i guess? not sure the details of the // forward method, but this computes everything like magic! Mat detectionMat = net.forward("detection_out"); - // log.info("output detection matrix produced"); - log.debug("detection matrix computed"); // iterate the rows of the detection matrix. for (int i = 0; i < detectionMat.rows(); i++) { Mat currentRow = detectionMat.row(i); @@ -248,13 +206,11 @@ private ArrayList yoloFrame(IplImage frame) { // skip the noise continue; } - // System.out.println("\nCurrent row has " + currentRow.size().width() + // "=width " + currentRow.size().height() + "=height."); // currentRow.position(probability_index); // int probability_size = detectionMat.cols() - probability_index; // detectionMat; - // String className = getWithDefault(classNames, i); // System.out.print("\nROW (" + className + "): " + // currentRow.getFloatBuffer().get(4) + " -- \t\t"); @@ -275,69 +231,57 @@ private ArrayList yoloFrame(IplImage frame) { // ok. in theory this is something we think it might actually be. float x = currentRow.getFloatBuffer().get(0); float y = currentRow.getFloatBuffer().get(1); - float width = currentRow.getFloatBuffer().get(2); float height = currentRow.getFloatBuffer().get(3); int xLeftBottom = (int) ((x - width / 2) * inputMat.cols()); int yLeftBottom = (int) ((y - height / 2) * inputMat.rows()); int xRightTop = (int) ((x + width / 2) * inputMat.cols()); int yRightTop = (int) ((y + height / 2) * inputMat.rows()); - if (xLeftBottom < 0) { xLeftBottom = 0; } if (yLeftBottom < 0) { yLeftBottom = 0; } - // crop the right top if (xRightTop > inputMat.cols()) { xRightTop = inputMat.cols(); } - if (yRightTop > inputMat.rows()) { yRightTop = inputMat.rows(); } - log.debug(label + " (" + confidence + "%) [(" + xLeftBottom + "," + yLeftBottom + "),(" + xRightTop + "," + yRightTop + ")]"); Rect boundingBox = new Rect(xLeftBottom, yLeftBottom, xRightTop - xLeftBottom, yRightTop - yLeftBottom); // grab just the bytes for the ROI defined by that rect.. // get that as a mat, save it as a byte array (png?) other encoding? // TODO: have a target size? - - IplImage cropped = extractSubImage(inputMat, boundingBox); if (debug) { debug = false; + IplImage cropped = extractSubImage(inputMat, boundingBox); show(cropped, "detected img"); } Classification obj = new Classification(String.format("%s.%s-%d", data.getName(), name, data.getFrameIndex())); obj.setLabel(label); obj.setBoundingBox(xLeftBottom, yLeftBottom, xRightTop - xLeftBottom, yRightTop - yLeftBottom); obj.setConfidence(confidence); + // TODO: add the original frame converted as a serializable image ( BufferedImage or png byte array? ) // obj.setImage(data.getDisplay()); - // for non-serializable "local" image objects - obj.setObject(frame); + // we might just want to provide a reference to the frame. such as the frame number or something similar so if + // we want to find the original frame we can look it up. (in solr?) yoloObjects.add(obj); } } } - log.info("yoloFrame - end"); return yoloObjects; } private IplImage extractSubImage(Mat inputMat, Rect boundingBox) { - log.info("extractSubImage - begin"); - // log.debug(boundingBox.x() + " " + boundingBox.y() + " " + boundingBox.width() + " " + boundingBox.height()); - // TODO: figure out if the width/height is too large! don't want to go array // out of bounds Mat cropped = new Mat(inputMat, boundingBox); - IplImage image = converterToIpl.convertToIplImage(converterToIpl.convert(cropped)); // This mat should be the cropped image! - - log.info("extractSubImage - end"); return image; } @@ -346,10 +290,8 @@ public void release() { // synchronized (lock) { log.info("release - begin"); disable(); // blocks until ready - // while(isRunning){ sleep(30) .. check again } // bleed out the thread before deallocating - if (net != null) { net.deallocate(); net = null; @@ -358,8 +300,6 @@ public void release() { // } } - transient volatile Object lock = new Object(); - @Override public void enable() { if (classifier != null) { @@ -396,16 +336,13 @@ public void disable() { @Override public BufferedImage processDisplay(Graphics2D graphics, BufferedImage image) { if (lastResult != null) { - for (Classification obj : lastResult) { String label = obj.getLabel() + " (" + df2.format(obj.getConfidence() * 100) + "%)"; - Rectangle bb = obj.getBoundingBox(); int x = (int) bb.x; int y = (int) bb.y; int width = (int) bb.width; int height = (int) bb.height; - graphics.setColor(Color.BLACK); graphics.drawRect(x, y, width, height); graphics.fillRect(x, y - 20, 7 * label.length(), 20); From 70eae53eeedf04c5354ca0911e748dc459ddecdd Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 12 Oct 2023 08:22:13 -0700 Subject: [PATCH 050/232] added deprecated onMoveX methods --- src/main/java/org/myrobotlab/service/InMoov2Arm.java | 5 +++++ src/main/java/org/myrobotlab/service/InMoov2Hand.java | 6 ++++++ src/main/java/org/myrobotlab/service/InMoov2Head.java | 6 ++++++ src/main/java/org/myrobotlab/service/InMoov2Torso.java | 6 ++++++ 4 files changed, 23 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java index 6305eb984a..d190af46e8 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java @@ -89,6 +89,11 @@ public static DHRobotArm getDHRobotArm(String name, String side) { return arm; } + @Deprecated /* use onMove */ + public void onMoveArm(HashMap map) { + onMove(map); + } + public void onMove(Map map) { moveTo(map.get("bicep"), map.get("rotate"), map.get("shoulder"), map.get("omoplate")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java index a4c1b35bae..9bf4cf923b 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -489,6 +490,11 @@ public LeapData onLeapData(LeapData data) { return data; } + @Deprecated /* use onMove */ + public void onMoveHand(HashMap map) { + onMove(map); + } + public void onMove(Map map) { moveTo(map.get("thumb"), map.get("index"), map.get("majeure"), map.get("majeure"), map.get("pinky"), map.get("wrist")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java index 1035f22995..0df855a1ab 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; @@ -218,6 +219,11 @@ public void lookAt(Double x, Double y, Double z) { log.info("object distance is {},rothead servo {},neck servo {} ", distance, rotation, colatitude); } + @Deprecated /* use onMoov */ + public void onMoveHead(HashMap map) { + onMove(map); + } + public void onMove(Map map) { moveTo(map.get("neck"), map.get("rothead"), map.get("eyeX"), map.get("eyeY"), map.get("jaw"), map.get("rollNeck")); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java index d6f0fcc1c4..f3953699c5 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import org.myrobotlab.framework.Service; @@ -93,6 +94,11 @@ public void disable() { lowStom.disable(); } + @Deprecated /* use onMove */ + public void onMoveTorso(HashMap map) { + onMove(map); + } + public void onMove(Map map) { moveTo(map.get("topStom"), map.get("midStom"), map.get("lowStom")); } From a0a4002d932dcd4ec6589c353d01f0af547b7788 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 12 Oct 2023 12:25:26 -0700 Subject: [PATCH 051/232] removed old config --- .../myrobotlab/service/config/VertXConfig.java | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 src/main/java/org/myrobotlab/service/config/VertXConfig.java diff --git a/src/main/java/org/myrobotlab/service/config/VertXConfig.java b/src/main/java/org/myrobotlab/service/config/VertXConfig.java deleted file mode 100644 index 6369ef70b5..0000000000 --- a/src/main/java/org/myrobotlab/service/config/VertXConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.myrobotlab.service.config; - -import java.util.ArrayList; -import java.util.List; - -public class VertXConfig extends ServiceConfig { - - public Integer port = 8443; - public boolean autoStartBrowser = true; - public List resources = new ArrayList<>(); - - public VertXConfig() { - // robot-x-app build directory - // resources.add("./resource/WebGui/app"); - } - -} From 6d7c2648bf36bc13e0c68b26bb6940b8ca99cf40 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 15 Oct 2023 09:43:35 -0700 Subject: [PATCH 052/232] removed unused var, added if release cond --- .github/workflows/build.yml | 5 +++-- src/main/java/org/myrobotlab/service/InMoov2.java | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce89b8e47b..f2d09bb420 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,6 @@ name: Java CI on: push: - branches: - - develop jobs: build: @@ -28,6 +26,7 @@ jobs: run: mvn --batch-mode -Dtest=!**/OpenCV* test -q - name: Get next version + if: github.ref == 'refs/heads/develop' uses: reecetech/version-increment@2023.9.3 id: version with: @@ -35,6 +34,7 @@ jobs: increment: patch - name: Package with Maven + if: github.ref == 'refs/heads/develop' run: "mvn package -DskipTests -Dversion=${{ steps.version.outputs.version }} -q" # - name: Fake Build @@ -43,6 +43,7 @@ jobs: # echo ${{ github.sha }} > ./target/myrobotlab.zip - name: Release + if: github.ref == 'refs/heads/develop' id: release uses: softprops/action-gh-release@v1 with: diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index afa9feb429..ee480bedd6 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -189,7 +189,6 @@ public static void main(String[] args) { protected String voiceSelected; - protected boolean wasMutedBeforeBoot = false; public InMoov2(String n, String id) { super(n, id); From 0574c5cf4315b25c9e8d4b6594575964aa248ff9 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 15 Oct 2023 09:57:14 -0700 Subject: [PATCH 053/232] fixed ivy --- .../java/org/myrobotlab/framework/Service.java | 18 +++++++++--------- .../myrobotlab/framework/repo/IvyWrapper.java | 10 +++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index c5f5cc22ac..5e6ef5ef10 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -725,8 +725,8 @@ public void addListener(MRLListener listener) { } @Override - public void addListener(String topicMethod, String callbackName) { - addListener(topicMethod, callbackName, CodecUtils.getCallbackTopicName(topicMethod)); + public void addListener(String localMethod, String remoteName) { + addListener(localMethod, remoteName, CodecUtils.getCallbackTopicName(localMethod)); } /** @@ -734,18 +734,18 @@ public void addListener(String topicMethod, String callbackName) { * "subscribe" from a different service FIXME !! - implement with HashMap or * HashSet .. WHY ArrayList ??? * - * @param topicMethod + * @param localMethod * - method when called, it's return will be sent to the - * callbackName/calbackMethod - * @param callbackName + * remoteName.remoteMethod + * @param remoteName * - name of the service to send return message to - * @param callbackMethod + * @param remoteMethod * - name of the method to send return data to */ @Override - public void addListener(String topicMethod, String callbackName, String callbackMethod) { - callbackName = CodecUtils.getFullName(callbackName); - MRLListener listener = new MRLListener(topicMethod, callbackName, callbackMethod); + public void addListener(String localMethod, String remoteName, String remoteMethod) { + remoteName = CodecUtils.getFullName(remoteName); + MRLListener listener = new MRLListener(localMethod, remoteName, remoteMethod); if (outbox.notifyList.containsKey(listener.topicMethod)) { // iterate through all looking for duplicate boolean found = false; diff --git a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java index 3d4045aa01..5c8da6bdea 100644 --- a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java @@ -520,13 +520,9 @@ synchronized public void install(String location, String[] serviceTypes) throws // IvyPatternHelper.substitute("[originalname].[ext]", // artifact); - ArtifactDownloadReport[] artifacts = report.getAllArtifactsReports(); - for (int i = 0; i < artifacts.length; ++i) { - ArtifactDownloadReport ar = artifacts[i]; - Artifact artifact = ar.getArtifact(); - // String filename = - // IvyPatternHelper.substitute("[originalname].[ext]", - // artifact); + File file = ar.getLocalFile(); + String filename = file.getAbsoluteFile().getAbsolutePath(); + log.info("{}", filename); if ("zip".equalsIgnoreCase(artifact.getExt())) { info("unzipping %s", filename); From ef2e9d02354f03788ef1b84e6942d5ef4a1def63 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 15 Oct 2023 10:30:46 -0700 Subject: [PATCH 054/232] pre release in build --- .github/workflows/build.yml | 18 ++++++++++++++---- .../service/config/InMoov2Config.java | 15 ++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2d09bb420..0f580a8543 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,5 @@ name: Java CI -#on: [push] - on: push: @@ -26,7 +24,6 @@ jobs: run: mvn --batch-mode -Dtest=!**/OpenCV* test -q - name: Get next version - if: github.ref == 'refs/heads/develop' uses: reecetech/version-increment@2023.9.3 id: version with: @@ -34,7 +31,6 @@ jobs: increment: patch - name: Package with Maven - if: github.ref == 'refs/heads/develop' run: "mvn package -DskipTests -Dversion=${{ steps.version.outputs.version }} -q" # - name: Fake Build @@ -42,6 +38,19 @@ jobs: # mkdir -p target # echo ${{ github.sha }} > ./target/myrobotlab.zip + - name: Pre Release + if: github.ref != 'refs/heads/develop' + id: prerelease + uses: softprops/action-gh-release@v1 + with: + token: ${{ secrets.ACCESS_TOKEN }} + prerelease: true + files: ./target/myrobotlab.zip + name: "Pre ${{ steps.version.outputs.version }} Nixie" + tag_name: ${{ steps.version.outputs.version }} + generate_release_notes: true + body_path: ./release-template.md + - name: Release if: github.ref == 'refs/heads/develop' id: release @@ -53,3 +62,4 @@ jobs: tag_name: ${{ steps.version.outputs.version }} generate_release_notes: true body_path: ./release-template.md + diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 6ff89b5629..3082aa2819 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -212,6 +212,7 @@ public Plan getDefault(Plan plan, String name) { } mouthControl.mouth = i01Name + ".mouth"; + ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot")); Runtime runtime = Runtime.getInstance(); @@ -255,13 +256,6 @@ public Plan getDefault(Plan plan, String name) { mouth.voice = "Mark"; mouth.speechRecognizers = new String[] { name + ".ear" }; - // == Peer - servoMixer ============================= - // setup name references to different services - ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer")); - servoMixer.listeners = new ArrayList<>(); - servoMixer.listeners.add(new Listener("publishText", name + ".mouth", "onText")); - //servoMixer.listeners.add(new Listener("publishText", name + ".chatBot", "onText")); - // == Peer - ear ============================= // setup name references to different services WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear")); @@ -542,6 +536,13 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + // service --to--> service + ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer")); + servoMixer.listeners = new ArrayList<>(); + servoMixer.listeners.add(new Listener("publishText", name + ".mouth", "onText")); + + + // remove the auto-added starts in the plan's runtime RuntimConfig.registry // plan.removeStartsWith(name + "."); From 688ab9c985e732ea83e5e80413076d32a1b80fab Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 15 Oct 2023 10:45:30 -0700 Subject: [PATCH 055/232] testing maven cache --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f580a8543..1a4ee1f233 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,8 @@ jobs: with: java-version: '11' distribution: 'adopt' + # NEATO ! CACHE !!! + cache: 'maven' - name: Dependency Test run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q From e8a4dbfebaddc30d4875ad9990941c80eee5b94f Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 15 Oct 2023 11:07:04 -0700 Subject: [PATCH 056/232] checking for cache improvements --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a4ee1f233..dd8286ce8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: with: java-version: '11' distribution: 'adopt' - # NEATO ! CACHE !!! + # NEATO ! CACHE !!!! cache: 'maven' - name: Dependency Test From 25e898b66329c5357d03fdd5d232a869c6ad3d1e Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 16 Oct 2023 12:54:41 -0700 Subject: [PATCH 057/232] wip --- src/main/java/org/myrobotlab/service/Vertx.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java index 8665793d37..09cd1132ad 100644 --- a/src/main/java/org/myrobotlab/service/Vertx.java +++ b/src/main/java/org/myrobotlab/service/Vertx.java @@ -149,6 +149,7 @@ public Map getClients() { @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()) { @@ -166,4 +167,8 @@ public void sendRemote(Message msg) throws Exception { @Override public boolean isLocal(Message msg) { return Runtime.getInstance().isLocal(msg); } + + public io.vertx.core.Vertx getVertx() { + return vertx; + } } From 84a036707c944fe390dc7e763ef142aa6daf52c7 Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 16 Oct 2023 12:55:11 -0700 Subject: [PATCH 058/232] added correct config --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4fa1d43508..563d633b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -116,7 +116,7 @@ build/ /lastRestart.py /.factorypath start.yml -config +/config src/main/resources/resource/InMoov2 src/main/resources/resource/ProgramAB *.iml From da541c4440d3c8f601459e6002c5fd940df62175 Mon Sep 17 00:00:00 2001 From: grog Date: Mon, 16 Oct 2023 12:56:58 -0700 Subject: [PATCH 059/232] wip --- .../java/org/myrobotlab/service/OpenCV.java | 49 ++++++++++++++++++- .../org/myrobotlab/service/ServoMixer.java | 16 ++++-- .../service/config/InMoov2Config.java | 6 +-- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java index 7abc45de8b..ba1edc8f93 100644 --- a/src/main/java/org/myrobotlab/service/OpenCV.java +++ b/src/main/java/org/myrobotlab/service/OpenCV.java @@ -55,6 +55,8 @@ import javax.imageio.ImageIO; import javax.imageio.stream.MemoryCacheImageOutputStream; +import org.bytedeco.ffmpeg.global.avcodec; +import org.bytedeco.javacpp.avutil; import org.bytedeco.javacv.CanvasFrame; import org.bytedeco.javacv.FFmpegFrameRecorder; import org.bytedeco.javacv.Frame; @@ -148,6 +150,8 @@ public class OpenCV extends AbstractComputerVision implements Imag int vpId = 0; transient CanvasFrame canvasFrame = null; + + transient FFmpegFrameRecorder recorder = null; class VideoProcessor implements Runnable { @@ -168,6 +172,44 @@ synchronized public void run() { lengthInTime = grabber.getLengthInTime(); log.info("grabber {} started - length time {} length frames {}", grabberType, lengthInTime, lengthInFrames); + // create recorder + recorder = new FFmpegFrameRecorder("output.flv", grabber.getImageWidth(), grabber.getImageHeight()); + // recorder.setFormat("mp4"); + recorder.setFormat("flv"); + +// recorder.setFormat("ogg"); // Set the output format to Ogg +// recorder.setVideoCodec(avcodec.AV_CODEC_ID_THEORA); // Set the video codec to Theora +// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // Set pixel format +// +// recorder.setVideoCodec(avcodec.AV_CODEC_ID_THEORA); // Set the video codec to Theora +// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // Set pixel format +// recorder.start(); +// +// recorder.start(); + +// h264 +// recorder.setFormat("mp4"); +// recorder.setVideoQuality(10); +// recorder.setFrameRate(grabber1.getFrameRate()); +// recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + + + + + // webm +// recorder.setVideoCodecName("libvpx-vp9"); +// recorder.setFormat("webm"); +// recorder.setPixelFormat(org.bytedeco.javacv.Frame.DEPTH_UBYTE); + // recorder.setFrameRate(frameRate); + + + // not sure what this is .. but it works + // recorder.setFormat("ffm"); + + + recorder.start(); + + // Wait for the Kinect to heat up. int loops = 0; while (grabber.getClass() == OpenKinectFrameGrabber.class && lengthInFrames == 0 && loops < 200) { @@ -197,6 +239,8 @@ synchronized public void run() { ++frameIndex; data = new OpenCVData(getName(), frameStartTs, frameIndex, newFrame); + + recorder.record(newFrame); if (grabber.getClass().equals(OpenKinectFrameGrabber.class)) { // by default this framegrabber returns video @@ -1214,6 +1258,7 @@ private void processVideo(OpenCVData data) throws org.bytedeco.javacv.FrameGrabb BufferedImage b = data.getDisplay(); SerializableImage si = new SerializableImage(b, displayFilter, frameIndex); invoke("publishDisplay", si); + // sleep(1000); if (webViewer) { // broadcast(???) @@ -2085,8 +2130,8 @@ public static void main(String[] args) throws Exception { // Runtime.start("python", "Python"); OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV"); - OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr"); - cv.addFilter(fr); +// OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr"); +// cv.addFilter(fr); // OpenCVFilterTracker tracker = new OpenCVFilterTracker("tracker"); // cv.addFilter(tracker); // OpenCVFilterLKOpticalTrack lk = new OpenCVFilterLKOpticalTrack("lk"); diff --git a/src/main/java/org/myrobotlab/service/ServoMixer.java b/src/main/java/org/myrobotlab/service/ServoMixer.java index dc6c3f7127..03395f6cc6 100755 --- a/src/main/java/org/myrobotlab/service/ServoMixer.java +++ b/src/main/java/org/myrobotlab/service/ServoMixer.java @@ -676,14 +676,24 @@ public void stopService() { public static void main(String[] args) throws Exception { try { + LoggingFactory.init("WARN"); + + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + + + boolean done = true; + if (done) { + return; + } + + Runtime.main(new String[] { "--id", "admin"}); LoggingFactory.init("INFO"); Runtime.start("i01.head.rothead", "Servo"); Runtime.start("i01.head.neck", "Servo"); - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); Python python = (Python) Runtime.start("python", "Python"); ServoMixer mixer = (ServoMixer) Runtime.start("mixer", "ServoMixer"); } catch (Exception e) { diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 3082aa2819..d48554891b 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -536,11 +536,11 @@ public Plan getDefault(Plan plan, String name) { listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); - // service --to--> service + // service --to--> service ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer")); servoMixer.listeners = new ArrayList<>(); - servoMixer.listeners.add(new Listener("publishText", name + ".mouth", "onText")); - + servoMixer.listeners.add(new Listener("publishText", getPeerName("mouth"), "onText")); + // remove the auto-added starts in the plan's runtime RuntimConfig.registry From 88abadd6394d050a2fbde5548ef2bbe502d67227 Mon Sep 17 00:00:00 2001 From: Langevin Gael Date: Tue, 17 Oct 2023 13:50:17 +0200 Subject: [PATCH 060/232] publishHeartbeat --- src/main/java/org/myrobotlab/service/InMoov2.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index ee480bedd6..eac6ce52dd 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1498,6 +1498,14 @@ public double publishBatteryLevel(double d) { return d; } + /** FIXME with checking status, healthbeat, and fire events to the FSM. + * Checks battery level and sends a heartbeat flash on publishHeartbeat + * and onHeartbeat at a regular interval + * */ + public void publishHeartbeat() { + log.info("publishHeartbeat"); + } + public void publishBoot() { log.info("publishBoot"); } From 34159fb089bad09bd8062ab0bdd6005961b62e41 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 17 Oct 2023 08:29:13 -0700 Subject: [PATCH 061/232] fixed publishHeartbeat --- src/main/java/org/myrobotlab/service/InMoov2.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index ee480bedd6..0ae5bfe5c3 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1539,6 +1539,13 @@ public void publishInactivity() { log.info("publishInactivity"); fsm.fire("inactvity"); } + + /** + * simple heartbeat clock of InMoov2 to keep things moving + */ + public void publishHeartbeat() { + log.info("publishInactivity"); + } /** * A more extensible interface point than publishEvent FIXME - create From 7b94a649c179bb2d417f80cca612ed9bd638b7c0 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 17 Oct 2023 08:34:36 -0700 Subject: [PATCH 062/232] now double ... weird --- src/main/java/org/myrobotlab/service/InMoov2.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index c5bb64b8ba..1efd9beeb6 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1548,13 +1548,6 @@ public void publishInactivity() { fsm.fire("inactvity"); } - /** - * simple heartbeat clock of InMoov2 to keep things moving - */ - public void publishHeartbeat() { - log.info("publishInactivity"); - } - /** * A more extensible interface point than publishEvent FIXME - create * interface for this From 15ca9fc68984f1bdd0bfbf26abd1bd7f50761df2 Mon Sep 17 00:00:00 2001 From: Langevin Gael Date: Tue, 17 Oct 2023 19:24:28 +0200 Subject: [PATCH 063/232] yours was better --- src/main/java/org/myrobotlab/service/InMoov2.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 1efd9beeb6..599f1dec44 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1498,12 +1498,11 @@ public double publishBatteryLevel(double d) { return d; } - /** FIXME with checking status, healthbeat, and fire events to the FSM. - * Checks battery level and sends a heartbeat flash on publishHeartbeat - * and onHeartbeat at a regular interval - * */ + /** + * simple heartbeat clock of InMoov2 to keep things moving + */ public void publishHeartbeat() { - log.info("publishHeartbeat"); + log.info("publishInactivity"); } public void publishBoot() { From 5554a4ec87529d973ecac42c5ba7ab05fbfacd33 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 17 Oct 2023 10:45:01 -0700 Subject: [PATCH 064/232] heh ... --- src/main/java/org/myrobotlab/service/InMoov2.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 599f1dec44..9e851a80ea 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -1498,11 +1498,13 @@ public double publishBatteryLevel(double d) { return d; } - /** - * simple heartbeat clock of InMoov2 to keep things moving - */ + /** + * A heartbeat that continues to check status, and fire events to the FSM. + * Checks battery, flashes leds and processes all the configured checks in + * onHeartbeat at a regular interval + * */ public void publishHeartbeat() { - log.info("publishInactivity"); + log.info("publishHeartbeat"); } public void publishBoot() { From 50113455bf7d319de2ab5746a2ac049d5128514f Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 17 Oct 2023 12:50:45 -0700 Subject: [PATCH 065/232] readme update links --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fdc207c71b..04ce055cf7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Open Source Framework for Robotics and Creative Machine Control * Project Website http://myrobotlab.org * Project Discord https://discord.gg/AfScp5x8r5 -* Latest Build [Nixie 1.1.(Latest)](http://build.myrobotlab.org:8080/job/myrobotlab/job/develop/lastSuccessfulBuild/artifact/target/myrobotlab.zip) +* Latest Build [Nixie 1.1.(Latest)](https://github.com/MyRobotLab/myrobotlab/releases/latest/download/myrobotlab.zip) * Latest Javadocs [Javdocs](http://build.myrobotlab.org:8080/job/myrobotlab/job/develop/lastSuccessfulBuild/artifact/target/site/apidocs/org/myrobotlab/service/package-summary.html) ## Base Requirements @@ -18,9 +18,7 @@ You will need Java 11 or newer. If you are only running MyRobotLab you need the ## Download the myrobotlab.jar Download -latest [Nixie 1.1.X](http://build.myrobotlab.org:8080/job/myrobotlab/job/develop/lastSuccessfulBuild/artifact/target/myrobotlab.zip) - -stable [Manticore 1.0.2693](https://github.com/MyRobotLab/myrobotlab/releases/tag/1.0.2693) +latest [Nixie 1.1.X](https://github.com/MyRobotLab/myrobotlab/releases/latest/download/myrobotlab.zip) ## Running MyRobotLab From 4f47f23275d54647cd335e00a6448d57f32fef04 Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 21 Oct 2023 15:22:45 -0700 Subject: [PATCH 066/232] Peer config utilities --- pom.xml | 3 + .../org/myrobotlab/framework/Service.java | 254 ++++++++++++------ .../java/org/myrobotlab/service/InMoov2.java | 60 +++-- .../java/org/myrobotlab/service/OpenCV.java | 15 ++ .../java/org/myrobotlab/service/Runtime.java | 30 ++- .../service/config/InMoov2Config.java | 4 +- .../service/config/OpenCVConfig.java | 4 + .../resources/resource/WebGui/app/peer.js | 16 +- .../WebGui/app/service/js/RuntimeGui.js | 6 + .../resource/framework/pom.xml.template | 3 + 10 files changed, 270 insertions(+), 125 deletions(-) diff --git a/pom.xml b/pom.xml index a00bd0db4f..6e8fdf4829 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,9 @@ # fast build mvn -DskipTests package -o + # execute + mvn exec:java -Dexec.mainClass=org.myrobotlab.service.Runtime -Dexec.args="-s webgui WebGui intro Intro python Python" + # specific test mvn test -Dtest="org.myrobotlab.service.WebGuiTest#postTest" diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 08534f2081..d9290b1527 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -101,11 +101,11 @@ public abstract class Service implements Runnable, Seri protected MetaData serviceType; /** - * Config member - configuration of type {ServiceType}Config - * Runtime applys either the default config or a saved config during service creation + * Config member - configuration of type {ServiceType}Config Runtime applys + * either the default config or a saved config during service creation */ protected T config; - + private static final long serialVersionUID = 1L; transient public final static Logger log = LoggerFactory.getLogger(Service.class); @@ -190,8 +190,8 @@ public abstract class Service implements Runnable, Seri /** * This is the map of interfaces - its really "static" information, since its - * a definition. However, since serialization will not process statics - we are making - * it a member variable + * a definition. However, since serialization will not process statics - we + * are making it a member variable */ // FIXME - this should be a map protected Map interfaceSet; @@ -279,11 +279,7 @@ public static Object copyShallowFrom(Object target, Object source) { * ){ log.info("here"); } */ - if (Modifier.isPrivate(modifiers) - || fname.equals("log") - || Modifier.isTransient(modifiers) - || Modifier.isStatic(modifiers) - || Modifier.isFinal(modifiers)) { + if (Modifier.isPrivate(modifiers) || fname.equals("log") || Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) { log.debug("skipping {}", field.getName()); continue; } else { @@ -472,18 +468,17 @@ static public String getResourceDir(Class clazz, String additionalPath) { * to glue together * @return the full resolved path * - * FIXME - DO NOT USE STATIC !!!! - * all instances of services should be able to get the resource directory - * If its static and "configurable" then it needs an instance of Runtime - * which is not available. + * FIXME - DO NOT USE STATIC !!!! all instances of services should be + * able to get the resource directory If its static and "configurable" + * then it needs an instance of Runtime which is not available. * */ @Deprecated /* this should not be static - remove it */ static public String getResourceDir(String serviceType, String additionalPath) { - // setting resource directory + // setting resource directory String resourceDir = null; - + // stupid solution to get past static problem if (!"Runtime".equals(serviceType)) { resourceDir = Runtime.getInstance().getConfig().resource + fs + serviceType; @@ -495,6 +490,7 @@ static public String getResourceDir(String serviceType, String additionalPath) { } return resourceDir; } + /** * non static get resource path return the path to a resource - since the root * can change depending if in debug or runtime - it gets the appropriate root @@ -1015,47 +1011,49 @@ public String[] getMethodNames() { public Method[] getMethods() { return this.getClass().getMethods(); } - + /** - * Returns a map containing all interface names from the class hierarchy and the interface hierarchy of the - * current class. + * Returns a map containing all interface names from the class hierarchy and + * the interface hierarchy of the current class. * * @return A map containing all interface names. */ public Map getInterfaceSet() { - Map ret = new TreeMap<>(); - Set> visitedClasses = new HashSet<>(); - getAllInterfacesHelper(getClass(), ret, visitedClasses); - return ret; + Map ret = new TreeMap<>(); + Set> visitedClasses = new HashSet<>(); + getAllInterfacesHelper(getClass(), ret, visitedClasses); + return ret; } /** - * Recursively traverses the class hierarchy and the interface hierarchy to add all interface names to the - * specified map. + * Recursively traverses the class hierarchy and the interface hierarchy to + * add all interface names to the specified map. * - * @param c The class to start the traversal from. - * @param ret The map to store the interface names. - * @param visitedClasses A set to keep track of visited classes to avoid infinite loops. + * @param c + * The class to start the traversal from. + * @param ret + * The map to store the interface names. + * @param visitedClasses + * A set to keep track of visited classes to avoid infinite loops. */ private void getAllInterfacesHelper(Class c, Map ret, Set> visitedClasses) { - if (c != null && !visitedClasses.contains(c)) { - // Add interfaces from the current class - Class[] interfaces = c.getInterfaces(); - for (Class interfaze : interfaces) { - ret.put(interfaze.getName(), interfaze.getName()); - } - - // Add interfaces from interfaces implemented by the current class - for (Class interfaze : interfaces) { - getAllInterfacesHelper(interfaze, ret, visitedClasses); - } + if (c != null && !visitedClasses.contains(c)) { + // Add interfaces from the current class + Class[] interfaces = c.getInterfaces(); + for (Class interfaze : interfaces) { + ret.put(interfaze.getName(), interfaze.getName()); + } - // Recursively traverse the superclass hierarchy - visitedClasses.add(c); - getAllInterfacesHelper(c.getSuperclass(), ret, visitedClasses); + // Add interfaces from interfaces implemented by the current class + for (Class interfaze : interfaces) { + getAllInterfacesHelper(interfaze, ret, visitedClasses); } + + // Recursively traverse the superclass hierarchy + visitedClasses.add(c); + getAllInterfacesHelper(c.getSuperclass(), ret, visitedClasses); + } } - public Message getMsg() throws InterruptedException { return inbox.getMsg(); @@ -1314,12 +1312,11 @@ final public Object invokeOn(boolean blockLocally, Object obj, String methodName if (blockLocally) { Outbox outbox = null; if (obj instanceof ServiceInterface) { - outbox = ((ServiceInterface)obj).getOutbox(); + outbox = ((ServiceInterface) obj).getOutbox(); } else { return retobj; } - - + List subList = outbox.notifyList.get(methodName); // correct? get local (default?) gateway Runtime runtime = Runtime.getInstance(); @@ -1399,27 +1396,88 @@ public boolean isRunning() { /** * getConfig returns current config of the service. This default super method - * will also filter webgui subscriptions out, in addition for any local subscriptions it - * will remove the instance "id" from any service. The reason it removes the webgui - * subscriptions is to avoid overwelming the user when modifying config. UI subscriptions - * tend to be very numerous and not very useful to the user. The reason it removes the - * instance id from local subscriptions is to allow the config to be used with any instance. - * Unless the user is controlling instance id, its random every restart. + * will also filter webgui subscriptions out, in addition for any local + * subscriptions it will remove the instance "id" from any service. The reason + * it removes the webgui subscriptions is to avoid overwelming the user when + * modifying config. UI subscriptions tend to be very numerous and not very + * useful to the user. The reason it removes the instance id from local + * subscriptions is to allow the config to be used with any instance. Unless + * the user is controlling instance id, its random every restart. */ public T getConfig() { return config; } /** - * Super class apply using template type. The default assigns config of the templated type, and also - * add listeners from subscriptions found on the base class ServiceConfig.listeners + * Get a service's peer's configuration. This method is used to get the + * configuration of a peer service regarless if it is currently running or + * not. If the peer is running the configuration is pulled from the active + * peer service, if it is not currently running the configuration is read from + * the current config set's service configuration file, if that does not exist + * the default configuration for this peer is used. + * + * @param peerKey + * - key of the peer service. e.g. "opencv" in the case of + * i01."opencv" + * @return */ - public T apply(T c) { - config = c; - addConfigListeners(c); - return config; + public ServiceConfig getPeerConfig(String peerKey) { + String peerName = getPeerName(peerKey); + if (peerName == null) { + error("peer name not found for peer key %s", peerKey); + return null; + } + + ServiceInterface si = Runtime.getService(peerName); + if (si != null) { + // peer is currently running - get its config + return si.getConfig(); + } + + // peer is not currently running attempt to read from config + Runtime runtime = Runtime.getInstance(); + // read current service config for this peer service + ServiceConfig sc = runtime.readServiceConfig(peerName); + if (sc == null) { + error("peer service %s is defined, but %s.yml not available on filesystem", peerKey, peerName); + return null; + } + return sc; + } + + public void setPeerConfigValue(String peerKey, String fieldname, Object value) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + ServiceConfig sc = getPeerConfig(peerKey); + if (sc == null) { + error("invalid config for peer key %s field name %s", peerKey, fieldname); + return; + } + Field field = sc.getClass().getDeclaredField(fieldname); + field.set(sc, value); + savePeerConfig(peerKey, sc); + String peerName = getPeerName(peerKey); + ConfigurableService cs = (ConfigurableService) Runtime.getService(peerName); + if (cs != null) { + cs.apply(sc); // TODO - look for applies if its read from the file system + // it needs to update Runtime.plan + } + + // broadcast change + invoke("getPeerConfig", peerKey); + Runtime.getPlan().put(peerName, sc); + Runtime runtime = Runtime.getInstance(); + runtime.broadcastState(); } + /** + * Super class apply using template type. The default assigns config of the + * templated type, and also add listeners from subscriptions found on the base + * class ServiceConfig.listeners + */ + public T apply(T c) { + config = c; + addConfigListeners(c); + return config; + } /** * The basic ServiceConfig has a list of listeners. These are definitions of @@ -1436,7 +1494,8 @@ public ServiceConfig addConfigListeners(ServiceConfig config) { } /** - * Default filtered config, used when saving, can be overriden by concrete class + * Default filtered config, used when saving, can be overriden by concrete + * class */ @Override public ServiceConfig getFilteredConfig() { @@ -1467,7 +1526,6 @@ public ServiceConfig getFilteredConfig() { return sc; } - @Override public void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { log.info("setting field name fieldname {} to {}", fieldname, value); @@ -1475,6 +1533,7 @@ public void setConfigValue(String fieldname, Object value) throws IllegalArgumen Field field = getConfig().getClass().getDeclaredField(fieldname); // field.setAccessible(true); should not need this - it "should" be public field.set(getConfig(), value); + save(); } @Override @@ -1579,11 +1638,12 @@ public void removeListener(String outMethod, String serviceName, String inMethod return true; } - // Previously we were not checking inMethod, which meant if a service had multiple - // subscriptions to the same topic (one to many mapping), the first in the list would be removed + // Previously we were not checking inMethod, which meant if a service + // had multiple + // subscriptions to the same topic (one to many mapping), the first in + // the list would be removed // instead of the requested one. - if (listener.callbackMethod.equals(inMethod) - && CodecUtils.checkServiceNameEquality(listener.callbackName, fullName)) { + if (listener.callbackMethod.equals(inMethod) && CodecUtils.checkServiceNameEquality(listener.callbackName, fullName)) { log.info("removeListener requested {}.{} to be removed", fullName, outMethod); return true; } @@ -1659,6 +1719,24 @@ public boolean save() { return runtime.saveService(runtime.getConfigName(), getName(), null); } + /** + * Save a service's peer's config to current config set + * + * @param peerKey + */ + public void savePeerConfig(String peerKey, ServiceConfig config) { + try { + Runtime runtime = Runtime.getInstance(); + String peerName = getPeerName(peerKey); + String data = CodecUtils.toYaml(config); + String ymlFileName = runtime.getConfigPath() + fs + CodecUtils.getShortName(peerName) + ".yml"; + FileIO.toFile(ymlFileName, data.getBytes()); + info("saved %s", ymlFileName); + } catch (Exception e) { + error(e); + } + } + public ServiceInterface getPeer(String peerKey) { String actualName = getPeerName(peerKey); return Runtime.getService(actualName); @@ -2009,7 +2087,7 @@ public void subscribe(String topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); subscribe(topicName, topicMethod, getFullName(), callbackMethod); } - + @Override public void subscribe(String service, String method, String callback) { subscribe(service, method, getFullName(), callback); @@ -2068,7 +2146,7 @@ public void unsubscribe(String topicName, String topicMethod) { String callbackMethod = CodecUtils.getCallbackTopicName(topicMethod); unsubscribe(topicName, topicMethod, getFullName(), callbackMethod); } - + @Override public void unsubscribe(String topicName, String topicMethod, String callback) { unsubscribe(topicName, topicMethod, getFullName(), callback); @@ -2097,12 +2175,7 @@ public Status error(Exception e) { @Override public Status error(String format, Object... args) { Status ret; - ret = Status.error( - String.format( - Objects.requireNonNullElse(format, ""), - args - ) - ); + ret = Status.error(String.format(Objects.requireNonNullElse(format, ""), args)); ret.name = getName(); log.error(ret.toString()); lastError = ret; @@ -2584,7 +2657,10 @@ public String localize(String key, Object... args) { } @Override - @Deprecated /* this system should be removed in favor of a ProgramAB instance with ability to translate */ + @Deprecated /* + * this system should be removed in favor of a ProgramAB instance + * with ability to translate + */ public void loadLocalizations() { if (defaultLocalization == null) { @@ -2756,16 +2832,36 @@ public void apply() { Runtime runtime = Runtime.getInstance(); String configName = runtime.getConfigName(); ServiceConfig sc = runtime.readServiceConfig(configName, name); - + if (sc == null) { error("config file %s not found", Runtime.getConfigRoot() + fs + configName + fs + name + ".yml"); return; } - - // updating plan + + // updating plan - FIXME remove plan Runtime.getPlan().put(getName(), sc); + // applying config to self - apply((T)sc); + apply((T) sc); + } + + /** + * Apply the config to a peer, regardless if the peer is currently running or + * not + * + * @param peerKey + * @param config + */ + public void applyPeerConfig(String peerKey, ServiceConfig config) { + String peerName = getPeerName(peerKey); + + Runtime.getPlan().put(peerName, config); + + // meh - templating is not very helpful here + ConfigurableService si = (ConfigurableService) Runtime.getService(peerName); + if (si != null) { + si.apply(config); + } } /** @@ -2786,11 +2882,11 @@ public void setPeerName(String key, String fullName) { // should we also make or update a config file - if the config path is set? info("updated %s name to %s", oldName, peer.name); } - + /** * get all the subscriptions to this service */ - public Map> getNotifyList(){ + public Map> getNotifyList() { return getOutbox().getNotifyList(); } diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 03035b6d2f..e9fa9d975e 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -140,11 +140,11 @@ public static boolean loadFile(String file) { protected String voiceSelected; - public InMoov2(String n, String id) { super(n, id); } + // should be removed in favor of general listeners public void addTextListener(TextListener service) { // CORRECT WAY ! - no direct reference - just use the name in a subscription addListener("publishText", service.getName()); @@ -407,7 +407,7 @@ public String execGesture(String gesture) { subscribe("python", "publishStatus", this.getName(), "onGestureStatus"); startedGesture(gesture); lastGestureExecuted = gesture; - Python python = (Python)Runtime.getService("python"); + Python python = (Python) Runtime.getService("python"); if (python == null) { error("python service not started"); return null; @@ -773,19 +773,19 @@ public void moveHead(Integer neck, Integer rothead, Integer rollNeck) { moveHead((double) neck, (double) rothead, null, null, null, (double) rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead) { + public void moveHeadBlocking(Double neck, Double rothead) { moveHeadBlocking(neck, rothead, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { + public void moveHeadBlocking(Double neck, Double rothead, Double rollNeck) { moveHeadBlocking(neck, rothead, null, null, null, rollNeck); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw) { moveHeadBlocking(neck, rothead, eyeX, eyeY, jaw, null); } - public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { + public void moveHeadBlocking(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { try { sendBlocking(getPeerName("head"), "moveToBlocking", neck, rothead, eyeX, eyeY, jaw, rollNeck); } catch (Exception e) { @@ -866,7 +866,7 @@ public void onGestureStatus(Status status) { error("I cannot execute %s, please check logs", lastGestureExecuted); } finishedGesture(lastGestureExecuted); - + unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } @@ -994,10 +994,10 @@ public void onStarted(String name) { Runtime runtime = Runtime.getInstance(); log.info("onStarted {}", name); -// BAD IDEA - better to ask for a system report or an error report -// if (runtime.isProcessingConfig()) { -// invoke("publishEvent", "CONFIG STARTED"); -// } + // BAD IDEA - better to ask for a system report or an error report + // if (runtime.isProcessingConfig()) { + // invoke("publishEvent", "CONFIG STARTED"); + // } String peerKey = getPeerKey(name); if (peerKey == null) { @@ -1229,8 +1229,8 @@ public String publishHeartbeat() { } /** - * A more extensible interface point than publishEvent - * FIXME - create interface for this + * A more extensible interface point than publishEvent FIXME - create + * interface for this * * @param msg * @return @@ -1561,7 +1561,7 @@ public void setTorsoSpeed(Integer topStom, Integer midStom, Integer lowStom) { setTorsoSpeed((double) topStom, (double) midStom, (double) lowStom); } - @Deprecated + @Deprecated /* use setTorsoSpeed */ public void setTorsoVelocity(Double topStom, Double midStom, Double lowStom) { setTorsoSpeed(topStom, midStom, lowStom); } @@ -1814,8 +1814,7 @@ public void startService() { subscribe("runtime", "shutdown"); // power up loopback subscription addListener(getName(), "powerUp"); - - + subscribe("runtime", "publishConfigList"); if (runtime.isProcessingConfig()) { invoke("publishEvent", "configStarted"); @@ -1923,7 +1922,6 @@ public void waitTargetPos() { sendToPeer("torso", "waitTargetPos"); } - public static void main(String[] args) { try { @@ -1934,20 +1932,26 @@ public static void main(String[] args) { // Runtime.startConfig("pr-1213-1"); - Runtime.main(new String[] {"--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python"}); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + // webgui.setSsl(true); + webgui.autoStartBrowser(false); + // webgui.setPort(8888); + webgui.startService(); + + InMoov2 i01 = (InMoov2)Runtime.start("i01","InMoov2"); + i01.setPeerConfigValue("opencv", "flip", true); + // i01.savePeerConfig("", null); + // Runtime.startConfig("default"); + + // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + boolean done = true; if (done) { return; } - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - // webgui.setSsl(true); - webgui.autoStartBrowser(false); - // webgui.setPort(8888); - webgui.startService(); - Runtime.start("python", "Python"); // Runtime.start("ros", "Ros"); Runtime.start("intro", "Intro"); @@ -1959,7 +1963,6 @@ public static void main(String[] args) { // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly"); // i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - // polly.speakBlocking("Hi, to be or not to be that is the question, // wheather to take arms against a see of trouble, and by aposing them end // them, to sleep, to die"); @@ -1999,9 +2002,9 @@ public static void main(String[] args) { random.save(); -// i01.startChatBot(); -// -// i01.startAll("COM3", "COM4"); + // i01.startChatBot(); + // + // i01.startAll("COM3", "COM4"); Runtime.start("python", "Python"); } catch (Exception e) { @@ -2009,4 +2012,5 @@ public static void main(String[] args) { } } + } diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java index 7abc45de8b..ff093721bd 100644 --- a/src/main/java/org/myrobotlab/service/OpenCV.java +++ b/src/main/java/org/myrobotlab/service/OpenCV.java @@ -1967,6 +1967,19 @@ public void enableFilter(String name) { } } + /** + * flip the video display vertically + * @param toFlip + */ + public void flip(boolean toFlip) { + config.flip = toFlip; + if (config.flip) { + addFilter("Flip"); + } else { + removeFilter("Flip"); + } + } + @Override public void disableFilter(String name) { OpenCVFilter f = filters.get(name); @@ -2062,6 +2075,8 @@ public OpenCVConfig apply(OpenCVConfig c) { // TODO: better configuration of the filter when it's added. } } + + flip(c.flip); if (c.capturing) { capture(); diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index 3d8a92f2cc..8f2035fbd6 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -141,7 +141,7 @@ public class Runtime extends Service implements MessageListener, * all these requests. */ @Deprecated /* use the filesystem only no memory plan */ - transient final Plan masterPlan = new Plan("runtime"); + protected final Plan masterPlan = new Plan("runtime"); /** * thread for non-blocking install of services @@ -2661,6 +2661,7 @@ synchronized static public ServiceInterface start(String name, String type) { } Runtime.load(name, type); + Runtime.savePlan(null); // FIXME - does some order need to be maintained Map services = createServicesFromPlan(Runtime.getPlan(), null, name); @@ -4805,6 +4806,16 @@ synchronized private Plan loadService(Plan plan, String name, String type, boole return plan; } + + /** + * read a service's configuration, in the context + * of current config set name or default + * @param name + * @return + */ + public ServiceConfig readServiceConfig(String name) { + return readServiceConfig(null, name); + } /** * @@ -4845,19 +4856,6 @@ public String publishConfigLoaded(String name) { return name; } - // @Override - // public ServiceConfig getConfig() { - // RuntimeConfig config = (RuntimeConfig)super.getConfig(); - // List listeners = new - // ArrayList - // for (org.myrobotlab.service.config.ServiceConfig.Listener listener: - // config.listeners) { - // if (listener.equals("stopped") || listener.equals("created")|| - // listener.equals("registered")|| listener.equals("released")) { - // - // } - // } - // } public String setAllIds(String id) { Platform.getLocalInstance().setId(id); @@ -5154,6 +5152,10 @@ private void savePlanInternal(String configName) { for (String s : masterPlan.keySet()) { String filename = CONFIG_ROOT + fs + configName + fs + s + ".yml"; + File check = new File(filename); + if (check.exists()) { + continue; + } String data = CodecUtils.toYaml(masterPlan.get(s)); try { FileIO.toFile(filename, data.getBytes()); diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index af16e23a04..9ebecf8262 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -52,9 +52,7 @@ public class InMoov2Config extends ServiceConfig { public boolean neoPixelFlashWhenSpeaking = true; public boolean openCVFaceRecognizerActivated=true; - - public boolean openCVFlipPicture=false; - + public boolean pirEnableTracking = false; /** diff --git a/src/main/java/org/myrobotlab/service/config/OpenCVConfig.java b/src/main/java/org/myrobotlab/service/config/OpenCVConfig.java index 2c786aa648..2087539836 100644 --- a/src/main/java/org/myrobotlab/service/config/OpenCVConfig.java +++ b/src/main/java/org/myrobotlab/service/config/OpenCVConfig.java @@ -15,5 +15,9 @@ public class OpenCVConfig extends ServiceConfig { public boolean webViewer = false; public boolean capturing = false; public Map filters = new LinkedHashMap<>(); + /** + * flip the video vertically + */ + public boolean flip = false; } diff --git a/src/main/resources/resource/WebGui/app/peer.js b/src/main/resources/resource/WebGui/app/peer.js index 78c15f5da3..e033693b52 100644 --- a/src/main/resources/resource/WebGui/app/peer.js +++ b/src/main/resources/resource/WebGui/app/peer.js @@ -4,7 +4,7 @@ console.info('peer') -angular.module('peer', []).service('peer', function( mrl /*$rootScope, $log*/ +angular.module('peer', []).service('peer', function( mrl ) { service = {}; @@ -40,6 +40,20 @@ angular.module('peer', []).service('peer', function( mrl /*$rootScope, $log*/ return null } + service.getPeerConfig = function(service, key) { + try { + if (service && service.config && service.config.peers && service.config.peers[key]){ + return mrl.getService('runtime').masterPlan['config'][service.config.peers[key].name] + // return mrl.getService(service.config.peers[key].name).config + } else { + // if peer is not started - return the filesystem config + } + } catch (error) { + console.error(error); + } + return null + } + service.changePeerTab = function(service, key) { try { diff --git a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js index 09eab9b6f6..4f452e9834 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js @@ -132,6 +132,10 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ _self.updateState(data) $scope.$apply() break + case 'onPlan': + $scope.plan = data + $scope.$apply() + break case 'onLocalServices': $scope.registry = data // $scope.$apply() @@ -408,6 +412,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ msg.subscribe("publishStatus") msg.subscribe('publishConfigList') msg.subscribe('publishInterfaceToNames') + // msg.subscribe("getPlan") //msg.send("getLocalServices") msg.send("getStartYml") @@ -417,6 +422,7 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ msg.send("getLocales") msg.send("publishInterfaceToNames") msg.send("getConfigName") + // msg.send("getPlan") // msg.send("getHosts") msg.subscribe(this) diff --git a/src/main/resources/resource/framework/pom.xml.template b/src/main/resources/resource/framework/pom.xml.template index ce45f6285a..f0ae2b4183 100644 --- a/src/main/resources/resource/framework/pom.xml.template +++ b/src/main/resources/resource/framework/pom.xml.template @@ -13,6 +13,9 @@ # fast build mvn -DskipTests package -o + # execute + mvn exec:java -Dexec.mainClass=org.myrobotlab.service.Runtime -Dexec.args="-s webgui WebGui intro Intro python Python" + # specific test mvn test -Dtest="org.myrobotlab.service.WebGuiTest#postTest" From 361b3e80d99146ab31a5bc2936f8ab9b8b3625f4 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 22 Oct 2023 06:59:40 -0700 Subject: [PATCH 067/232] adding build and ignore --- .github/workflows/build.yml | 39 ++----------------------------------- .gitignore | 3 +++ 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b8c8e5b4a..93d82942aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,6 @@ name: Java CI -#on: [push] - -on: - push: - branches: - - develop +on: [push] jobs: build: @@ -20,37 +15,7 @@ jobs: with: java-version: '11' distribution: 'adopt' - - name: Dependency Test run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q - - - name: Build and Test with Maven + - name: Build with Maven run: mvn --batch-mode -Dtest=!**/OpenCV* test -q - - - name: Get next version - uses: reecetech/version-increment@2023.9.3 - id: version - with: - scheme: semver - increment: patch - - - name: Package with Maven - run: "mvn package -DskipTests -Dversion=${{ steps.version.outputs.version }} -q" - - # - name: Fake Build - # run: | - # mkdir -p target - # echo ${{ github.sha }} > ./target/myrobotlab.zip - - - name: Release - id: release - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.ACCESS_TOKEN }} - files: | - ./target/myrobotlab.zip - ./target/myrobotlab.jar - name: "${{ steps.version.outputs.version }} Nixie" - tag_name: ${{ steps.version.outputs.version }} - generate_release_notes: true - body_path: ./release-template.md 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 From 2674733a26e91433f684b2351b92c8c134360050 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 22 Oct 2023 07:06:54 -0700 Subject: [PATCH 068/232] start of InMoov2Test --- .../org/myrobotlab/service/InMoov2Test.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/test/java/org/myrobotlab/service/InMoov2Test.java diff --git a/src/test/java/org/myrobotlab/service/InMoov2Test.java b/src/test/java/org/myrobotlab/service/InMoov2Test.java new file mode 100644 index 0000000000..9be0e28537 --- /dev/null +++ b/src/test/java/org/myrobotlab/service/InMoov2Test.java @@ -0,0 +1,28 @@ +package org.myrobotlab.service; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.myrobotlab.service.config.OpenCVConfig; + +public class InMoov2Test { + + @Test + public void testCvFilters() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + + InMoov2 i01 = (InMoov2)Runtime.start("i01", "InMoov2"); + + // flip + i01.setPeerConfigValue("opencv", "flip", true); + OpenCVConfig cvconfig = (OpenCVConfig)i01.getPeerConfig("opencv"); + assertTrue(cvconfig.flip); + + i01.setPeerConfigValue("opencv", "flip", false); + cvconfig = (OpenCVConfig)i01.getPeerConfig("opencv"); + assertFalse(cvconfig.flip); + + } + + +} + From 2380e7bd8c6dd44831bc48b0669461d006ecefa6 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 22 Oct 2023 07:23:59 -0700 Subject: [PATCH 069/232] clearing plan --- src/main/java/org/myrobotlab/service/Runtime.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index d8abae8ed5..c831f0ea0f 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -4294,8 +4294,7 @@ public Plan getLocalPlan() { */ static public void clearPlan() { Runtime runtime = Runtime.getInstance(); - // fixes concurrent modification - runtime.masterPlan = new Plan("runtime"); + runtime.masterPlan.clear(); runtime.masterPlan.put("runtime", new RuntimeConfig()); // unset config path runtime.configName = null; From 8149ae694df7e0eaebfc4fca61818113bfdeae66 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 24 Oct 2023 21:17:50 -0700 Subject: [PATCH 070/232] merged master --- pom.xml | 6 ++++ .../org/myrobotlab/framework/Service.java | 19 +++++++------ .../java/org/myrobotlab/service/WebGui.java | 2 +- .../java/org/myrobotlab/service/WebXR.java | 10 ++++++- .../service/config/InMoov2Config.java | 28 ++++++++++++++++++- .../org/myrobotlab/vertx/ApiVerticle.java | 12 ++++++-- 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 7acbce2abb..e6b9aa346c 100644 --- a/pom.xml +++ b/pom.xml @@ -198,6 +198,12 @@ + + org.nanohttpd + nanohttpd + 2.2.0 + + org.bytedeco diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java index 4016375a7a..36d3078f63 100644 --- a/src/main/java/org/myrobotlab/framework/Service.java +++ b/src/main/java/org/myrobotlab/framework/Service.java @@ -1416,7 +1416,8 @@ public

    P getPeerConfig(String peerKey, StaticType

    t } // Java generics don't let us create a new StaticType using - // P here because the type variable is erased, so we have to cast anyway for now + // P here because the type variable is erased, so we have to cast anyway for + // now ConfigurableService

    si = (ConfigurableService

    ) Runtime.getService(peerName); if (si != null) { // peer is currently running - get its config @@ -1447,7 +1448,8 @@ public void setPeerConfigValue(String peerKey, String fieldname, Object value) t field.set(sc, value); savePeerConfig(peerKey, sc); String peerName = getPeerName(peerKey); - var cs = Runtime.getConfigurableService(peerName, new StaticType>() {}); + var cs = Runtime.getConfigurableService(peerName, new StaticType>() { + }); if (cs != null) { cs.apply(sc); // TODO - look for applies if its read from the file system // it needs to update Runtime.plan @@ -2163,7 +2165,7 @@ public Status error(Exception e) { invoke("publishStatus", status); return status; } - + @Override public Status error(String format, Object... args) { Status ret; @@ -2194,10 +2196,8 @@ public Status warn(String msg) { @Override public Status warn(String format, Object... args) { - String msg = String.format( - Objects.requireNonNullElse(format, ""), - args); - + String msg = String.format(Objects.requireNonNullElse(format, ""), args); + return warn(msg); } @@ -2235,7 +2235,7 @@ public Status info(String format, Object... args) { public Status publishError(Status status) { return status; } - + public Status publishWarn(Status status) { return status; } @@ -2857,7 +2857,8 @@ public void apply() { } public void applyPeerConfig(String peerKey, ServiceConfig config) { - applyPeerConfig(peerKey, config, new StaticType<>() {}); + applyPeerConfig(peerKey, config, new StaticType<>() { + }); } /** diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java index dd2178bf8c..d007befe6e 100644 --- a/src/main/java/org/myrobotlab/service/WebGui.java +++ b/src/main/java/org/myrobotlab/service/WebGui.java @@ -1179,7 +1179,7 @@ public static void main(String[] args) { // Platform.setVirtual(true); - Runtime.startConfig("default"); + Runtime.startConfig("dev"); boolean done = true; if (done) { diff --git a/src/main/java/org/myrobotlab/service/WebXR.java b/src/main/java/org/myrobotlab/service/WebXR.java index 63a3f1f3ed..509620832e 100644 --- a/src/main/java/org/myrobotlab/service/WebXR.java +++ b/src/main/java/org/myrobotlab/service/WebXR.java @@ -129,7 +129,15 @@ public static void main(String[] args) { // identical to command line start // Runtime.startConfig("inmoov2"); - Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + + + // 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; diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index 6625e37096..340ed30780 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -1,10 +1,13 @@ package org.myrobotlab.service.config; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import org.myrobotlab.framework.Plan; import org.myrobotlab.jme3.UserDataConfig; import org.myrobotlab.math.MapperLinear; +import org.myrobotlab.math.MapperSimple; import org.myrobotlab.service.Pid.PidData; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition; @@ -198,7 +201,26 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "torso", "InMoov2Torso", false); addDefaultPeerConfig(plan, name, "ultrasonicRight", "UltrasonicSensor", false); addDefaultPeerConfig(plan, name, "ultrasonicLeft", "UltrasonicSensor", false); - + addDefaultPeerConfig(plan, name, "vertx", "Vertx", false); + addDefaultPeerConfig(plan, name, "webxr", "WebXR", false); + + WebXRConfig webxr = (WebXRConfig)plan.get(getPeerName("webxr")); + + Map map = new HashMap<>(); + MapperSimple mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.neck", mapper); + webxr.controllerMappings.put("head.orientation.pitch", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.rothead", mapper); + webxr.controllerMappings.put("head.orientation.yaw", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.roll", mapper); + webxr.controllerMappings.put("head.orientation.roll", map); + MouthControlConfig mouthControl = (MouthControlConfig) plan.get(getPeerName("mouthControl")); // setup name references to different services FIXME getPeerName("head").getPeerName("jaw") @@ -525,6 +547,10 @@ public Plan getDefault(Plan plan, String name) { mouth_audioFile.listeners = new ArrayList<>(); mouth_audioFile.listeners.add(new Listener("publishPeak", name)); fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); + + webxr.listeners = new ArrayList<>(); + webxr.listeners.add(new Listener("publishJointAngles", name)); + // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); diff --git a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java index 57239bd210..52c3d5d305 100644 --- a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -47,14 +47,20 @@ public void start() throws Exception { // 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" - //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/"); + // 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); From 4b939caff5d152b70fed77a6bbf3fc9866f601ef Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 25 Oct 2023 07:22:16 -0700 Subject: [PATCH 071/232] removing video handler for now --- src/main/java/org/myrobotlab/vertx/ApiVerticle.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java index 52c3d5d305..4dc470f1df 100644 --- a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java @@ -58,9 +58,9 @@ public void start() throws Exception { // root.setAllowRootFileSystemAccess(true); // root.setWebRoot(null); - VideoStreamHandler video = new VideoStreamHandler(service); + // VideoStreamHandler video = new VideoStreamHandler(service); - router.route("/video/*").handler(video); + // router.route("/video/*").handler(video); router.route("/*").handler(root); From fc63c5af6015fd8ae02830014bf743b51629f278 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 29 Oct 2023 06:53:50 -0700 Subject: [PATCH 072/232] readme update --- README.html | 9 +++++++++ assembly.xml | 1 + 2 files changed, 10 insertions(+) create mode 100644 README.html diff --git a/README.html b/README.html new file mode 100644 index 0000000000..2449979c5e --- /dev/null +++ b/README.html @@ -0,0 +1,9 @@ + + + +

    Starting MyRobotLab

    +

    Instructions Here


    +

    Help

    +

    Discord Channel


    + + \ No newline at end of file diff --git a/assembly.xml b/assembly.xml index 562bd86cf8..768fcbab9f 100644 --- a/assembly.xml +++ b/assembly.xml @@ -12,6 +12,7 @@ myrobotlab.bat myrobotlab.sh + README.html ./ myrobotlab-${version} From ec5ef4abec2d1cbfa1559dc695d91d91e1e63133 Mon Sep 17 00:00:00 2001 From: grog Date: Fri, 3 Nov 2023 07:50:14 -0700 Subject: [PATCH 073/232] updated mux config apply --- .../java/org/myrobotlab/service/I2cMux.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/I2cMux.java b/src/main/java/org/myrobotlab/service/I2cMux.java index c3403a1eb4..71e885787f 100644 --- a/src/main/java/org/myrobotlab/service/I2cMux.java +++ b/src/main/java/org/myrobotlab/service/I2cMux.java @@ -13,7 +13,6 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.I2cMuxConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.I2CControl; import org.myrobotlab.service.interfaces.I2CController; import org.slf4j.Logger; @@ -376,11 +375,23 @@ public I2cMuxConfig getConfig() { public I2cMuxConfig apply(I2cMuxConfig c) { super.apply(c); // FIXME - remove all this, it should "only" be in config - deviceBus = c.bus; - deviceAddress = c.address; - i2cDevices = c.i2cDevices; + if (c.bus != null) { + setBus(c.bus); + } + if (c.address != null) { + setAddress(c.address); + } + + if (c.i2cDevices != null) { + i2cDevices = c.i2cDevices; + } + if (c.controller != null) { - controllerName = c.controller; + try { + attach(controllerName); + } catch(Exception e) { + error(e); + } } return c; } From 29df9ee2629a42effb3777c58ec87b8946dfc487 Mon Sep 17 00:00:00 2001 From: grog Date: Fri, 3 Nov 2023 09:18:39 -0700 Subject: [PATCH 074/232] bumping --- src/main/java/org/myrobotlab/service/I2cMux.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/myrobotlab/service/I2cMux.java b/src/main/java/org/myrobotlab/service/I2cMux.java index 71e885787f..ced3b15a46 100644 --- a/src/main/java/org/myrobotlab/service/I2cMux.java +++ b/src/main/java/org/myrobotlab/service/I2cMux.java @@ -394,6 +394,7 @@ public I2cMuxConfig apply(I2cMuxConfig c) { } } return c; + } } From 8b468a040ed6c70712f4a04d44e033e43409c0b8 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 5 Nov 2023 06:38:07 -0800 Subject: [PATCH 075/232] removed changes from opencv --- .../java/org/myrobotlab/service/OpenCV.java | 49 +------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java index 88deb97360..ff093721bd 100644 --- a/src/main/java/org/myrobotlab/service/OpenCV.java +++ b/src/main/java/org/myrobotlab/service/OpenCV.java @@ -55,8 +55,6 @@ import javax.imageio.ImageIO; import javax.imageio.stream.MemoryCacheImageOutputStream; -import org.bytedeco.ffmpeg.global.avcodec; -import org.bytedeco.javacpp.avutil; import org.bytedeco.javacv.CanvasFrame; import org.bytedeco.javacv.FFmpegFrameRecorder; import org.bytedeco.javacv.Frame; @@ -150,8 +148,6 @@ public class OpenCV extends AbstractComputerVision implements Imag int vpId = 0; transient CanvasFrame canvasFrame = null; - - transient FFmpegFrameRecorder recorder = null; class VideoProcessor implements Runnable { @@ -172,44 +168,6 @@ synchronized public void run() { lengthInTime = grabber.getLengthInTime(); log.info("grabber {} started - length time {} length frames {}", grabberType, lengthInTime, lengthInFrames); - // create recorder - recorder = new FFmpegFrameRecorder("output.flv", grabber.getImageWidth(), grabber.getImageHeight()); - // recorder.setFormat("mp4"); - recorder.setFormat("flv"); - -// recorder.setFormat("ogg"); // Set the output format to Ogg -// recorder.setVideoCodec(avcodec.AV_CODEC_ID_THEORA); // Set the video codec to Theora -// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // Set pixel format -// -// recorder.setVideoCodec(avcodec.AV_CODEC_ID_THEORA); // Set the video codec to Theora -// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // Set pixel format -// recorder.start(); -// -// recorder.start(); - -// h264 -// recorder.setFormat("mp4"); -// recorder.setVideoQuality(10); -// recorder.setFrameRate(grabber1.getFrameRate()); -// recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); - - - - - // webm -// recorder.setVideoCodecName("libvpx-vp9"); -// recorder.setFormat("webm"); -// recorder.setPixelFormat(org.bytedeco.javacv.Frame.DEPTH_UBYTE); - // recorder.setFrameRate(frameRate); - - - // not sure what this is .. but it works - // recorder.setFormat("ffm"); - - - recorder.start(); - - // Wait for the Kinect to heat up. int loops = 0; while (grabber.getClass() == OpenKinectFrameGrabber.class && lengthInFrames == 0 && loops < 200) { @@ -239,8 +197,6 @@ synchronized public void run() { ++frameIndex; data = new OpenCVData(getName(), frameStartTs, frameIndex, newFrame); - - recorder.record(newFrame); if (grabber.getClass().equals(OpenKinectFrameGrabber.class)) { // by default this framegrabber returns video @@ -1258,7 +1214,6 @@ private void processVideo(OpenCVData data) throws org.bytedeco.javacv.FrameGrabb BufferedImage b = data.getDisplay(); SerializableImage si = new SerializableImage(b, displayFilter, frameIndex); invoke("publishDisplay", si); - // sleep(1000); if (webViewer) { // broadcast(???) @@ -2145,8 +2100,8 @@ public static void main(String[] args) throws Exception { // Runtime.start("python", "Python"); OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV"); -// OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr"); -// cv.addFilter(fr); + OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr"); + cv.addFilter(fr); // OpenCVFilterTracker tracker = new OpenCVFilterTracker("tracker"); // cv.addFilter(tracker); // OpenCVFilterLKOpticalTrack lk = new OpenCVFilterLKOpticalTrack("lk"); From 42c27dbb74194e175214f6bfd67b1e60e8f69157 Mon Sep 17 00:00:00 2001 From: GroG Date: Sun, 5 Nov 2023 06:44:50 -0800 Subject: [PATCH 076/232] Update README.html --- README.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.html b/README.html index 2449979c5e..2b5f39a3e5 100644 --- a/README.html +++ b/README.html @@ -6,4 +6,4 @@

    Starting MyRobotLab

    Help

    Discord Channel


    - \ No newline at end of file + From 312525166f9f6c5c3ea28524302db757fed7e11a Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 5 Nov 2023 07:04:07 -0800 Subject: [PATCH 077/232] merged develop --- .vscode/launch.json | 1 - .../java/org/myrobotlab/service/Runtime.java | 6 -- .../java/org/myrobotlab/service/WebXR.java | 54 +++++++++--------- .../org/myrobotlab/vertx/ApiVerticle.java | 5 ++ .../myrobotlab/vertx/WebSocketHandler.java | 8 ++- src/main/resources/resource/WebXR.png | Bin 20829 -> 2426 bytes src/main/resources/resource/WebXr.png | Bin 20829 -> 0 bytes 7 files changed, 39 insertions(+), 35 deletions(-) delete mode 100644 src/main/resources/resource/WebXr.png diff --git a/.vscode/launch.json b/.vscode/launch.json index e2826c6c69..421dbb64aa 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,6 @@ "-c", "dev" ] - } ] } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java index 7ab2febc18..bf4a155d1d 100644 --- a/src/main/java/org/myrobotlab/service/Runtime.java +++ b/src/main/java/org/myrobotlab/service/Runtime.java @@ -198,12 +198,6 @@ public class Runtime extends Service implements MessageListener, */ boolean processingConfig = false; - /** - * The one config directory where all config is managed the {default} is the - * current configuration set - */ - // protected String configDir = "data" + fs + "config"; - /** *
        * The set of client connections to this mrl instance Some of the connections
    diff --git a/src/main/java/org/myrobotlab/service/WebXR.java b/src/main/java/org/myrobotlab/service/WebXR.java
    index 509620832e..a9a642b82b 100644
    --- a/src/main/java/org/myrobotlab/service/WebXR.java
    +++ b/src/main/java/org/myrobotlab/service/WebXR.java
    @@ -25,9 +25,9 @@ public WebXR(String n, String id) {
       }
       
       public Event publishEvent(Event event) {
    -    if (log.isDebugEnabled()) {
    -      log.debug("publishEvent {}", event);
    -    }
    +    // if (log.isDebugEnabled()) {
    +      log.info("publishEvent XRController {}", event);
    +    // }
             
         String path = String.format("event.%s.%s", event.meta.get("handedness"), event.type);
         if (event.value != null) {
    @@ -53,9 +53,9 @@ public Event publishEvent(Event event) {
        * @return
        */
       public Pose publishPose(Pose pose) {
    -    if (log.isDebugEnabled()) {
    -      log.debug("publishPose {}", pose);
    -    }    
    +//    if (log.isDebugEnabled()) {
    +      log.error("publishPose {}", pose);
    +//    }    
         // process mappings config into joint angles
         Map map = new HashMap<>();
     
    @@ -83,27 +83,27 @@ public Pose publishPose(Pose pose) {
           }
         }
         
    -    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);
    -    }
    +//    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);
    diff --git a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java
    index 4dc470f1df..f506d72da3 100644
    --- a/src/main/java/org/myrobotlab/vertx/ApiVerticle.java
    +++ b/src/main/java/org/myrobotlab/vertx/ApiVerticle.java
    @@ -8,6 +8,7 @@
     import io.vertx.core.http.HttpMethod;
     import io.vertx.core.http.HttpServer;
     import io.vertx.core.http.HttpServerOptions;
    +import io.vertx.core.http.HttpServerRequest;
     import io.vertx.core.net.SelfSignedCertificate;
     import io.vertx.ext.web.Router;
     import io.vertx.ext.web.handler.CorsHandler;
    @@ -91,6 +92,10 @@ public void start() throws Exception {
         // FIXME - how to do this -> server.webSocketHandler(this::handleWebSocket);
         server.webSocketHandler(new WebSocketHandler(service));
         server.requestHandler(router);
    +
    +
    +
    +
         // start servers
         server.listen();
       }
    diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
    index 69b56957f3..17014c19db 100644
    --- a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
    +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
    @@ -11,6 +11,8 @@
     import org.slf4j.Logger;
     
     import io.vertx.core.Handler;
    +import io.vertx.core.MultiMap;
    +import io.vertx.core.http.HttpServerRequest;
     import io.vertx.core.http.ServerWebSocket;
     
     /**
    @@ -26,6 +28,8 @@ public class WebSocketHandler implements Handler {
     
       public final static Logger log = LoggerFactory.getLogger(WebSocketHandler.class);
     
    +  
    +
       /**
        * reference to the MRL Vertx service / websocket and http server
        */
    @@ -102,7 +106,9 @@ public WebSocketHandler(org.myrobotlab.service.Vertx service) {
       public void handle(ServerWebSocket socket) {
         // FIXME - get "id" from js client - need something unique from the js
         // client
    -    // String id = r.getRequest().getParameter("id");
    +    MultiMap headers = socket.headers();
    +    String uri = socket.uri();
    +    // String remoteId = r.getRequest().getParameter("id");
         String id = String.format("vertx-%s", service.getName());
         // String uuid = UUID.randomUUID().toString();
         String uuid = socket.binaryHandlerID();
    diff --git a/src/main/resources/resource/WebXR.png b/src/main/resources/resource/WebXR.png
    index 2432b77c92282b7a4f0f27c73b8556b998f14f19..dcf18c9242c5ce9943f40bf748911c5a80a98d67 100644
    GIT binary patch
    literal 2426
    zcmV-=35E8FP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006
    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
    
    diff --git a/src/main/resources/resource/WebXr.png b/src/main/resources/resource/WebXr.png
    deleted file mode 100644
    index 2432b77c92282b7a4f0f27c73b8556b998f14f19..0000000000000000000000000000000000000000
    GIT binary patch
    literal 0
    HcmV?d00001
    
    literal 20829
    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
    
    
    From c6a518dbeb81af31e22782d83ed1d70af4757024 Mon Sep 17 00:00:00 2001
    From: grog 
    Date: Sun, 5 Nov 2023 07:08:08 -0800
    Subject: [PATCH 078/232] merged in develop
    
    ---
     src/main/java/org/myrobotlab/service/InMoov2.java | 11 ++---------
     1 file changed, 2 insertions(+), 9 deletions(-)
    
    diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
    index 82b3514311..d74287f653 100644
    --- a/src/main/java/org/myrobotlab/service/InMoov2.java
    +++ b/src/main/java/org/myrobotlab/service/InMoov2.java
    @@ -1286,7 +1286,8 @@ public void onPirOn() {
         led.green = 100;
         led.blue = 150;
         led.count = 5;
    -    led.interval = 500;
    +    led.timeOn = 500;
    +    led.timeOff = 10;
         // FIXME flash on config.flashOnBoot
         invoke("publishFlash");
         String botState = chatBot.getPredicate("botState");
    @@ -1295,14 +1296,6 @@ public void onPirOn() {
         }    
       }
     
    -  /**
    -   * Pir on callback
    -   */
    -  public void onPirOn() {
    -    isPirOn = true;
    -    fsm.fire("wake");
    -  }
    -
       /**
        * Pir off callback
        */
    
    From efe51dbcb1d201a41c935159f5107271c94e89f1 Mon Sep 17 00:00:00 2001
    From: grog 
    Date: Sun, 5 Nov 2023 07:18:58 -0800
    Subject: [PATCH 079/232] synching with dev
    
    ---
     .gitignore                                                | 1 +
     README.html                                               | 2 +-
     src/main/java/org/myrobotlab/document/Classification.java | 8 ++++++++
     3 files changed, 10 insertions(+), 1 deletion(-)
    
    diff --git a/.gitignore b/.gitignore
    index e57bfc2d60..30215e9549 100644
    --- a/.gitignore
    +++ b/.gitignore
    @@ -94,3 +94,4 @@ build/
     /.factorypath
     start.yml
     /config
    +
    diff --git a/README.html b/README.html
    index 2b5f39a3e5..2449979c5e 100644
    --- a/README.html
    +++ b/README.html
    @@ -6,4 +6,4 @@ 

    Starting MyRobotLab

    Help

    Discord Channel


    - + \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/document/Classification.java b/src/main/java/org/myrobotlab/document/Classification.java index 727bbd36e0..2f615865a5 100644 --- a/src/main/java/org/myrobotlab/document/Classification.java +++ b/src/main/java/org/myrobotlab/document/Classification.java @@ -110,6 +110,14 @@ public BufferedImage getImage() { return null; } + public void setObject(Object frame) { + setField("imageObject", frame); + } + + public Object getObject() { + return getValue("imageObject"); + } + public void setBoundingBox(Rectangle rect) { setField("bounding_box", rect); } From 52381d30e97be17779afcc4c609219aba82e3b07 Mon Sep 17 00:00:00 2001 From: grog Date: Sun, 5 Nov 2023 07:43:15 -0800 Subject: [PATCH 080/232] more merging of develop --- .vscode/launch.json | 1626 ++++++++++++++++- .../org/myrobotlab/kinematics/DHRobotArm.java | 5 +- 2 files changed, 1618 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 421dbb64aa..0ad59dd7dd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,20 +4,1628 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "java", + "name": "Current File", + "request": "launch", + "mainClass": "${file}" + }, + { + "type": "java", + "name": "PeerDiscovery", + "request": "launch", + "mainClass": "PeerDiscovery", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ArduinoMsgGenerator", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.ArduinoMsgGenerator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Msg", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.Msg", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VirtualMsg", + "request": "launch", + "mainClass": "org.myrobotlab.arduino.VirtualMsg", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CaptureCalibrationImagesApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.CaptureCalibrationImagesApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CreateRgbPointCloudFileApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.CreateRgbPointCloudFileApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DisplayKinectPointCloudApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.DisplayKinectPointCloudApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ExampleDepthPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.ExampleDepthPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ExampleVisualOdometryDepth", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.ExampleVisualOdometryDepth", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IntrinsicToDepthParameters", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.IntrinsicToDepthParameters", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LogKinectDataApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.LogKinectDataApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectOdometry", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectOdometry", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenKinectStreamingTest", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OpenKinectStreamingTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OverlayRgbDepthStreamsApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.OverlayRgbDepthStreamsApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "PlaybackKinectLogApp", + "request": "launch", + "mainClass": "org.myrobotlab.boofcv.PlaybackKinectLogApp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CmdLine", + "request": "launch", + "mainClass": "org.myrobotlab.cmdline.CmdLine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CodecUtils", + "request": "launch", + "mainClass": "org.myrobotlab.codec.CodecUtils", + "projectName": "mrl" + }, + { + "type": "java", + "name": "PolymorphicSerializer", + "request": "launch", + "mainClass": "org.myrobotlab.codec.PolymorphicSerializer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RecorderPythonFile", + "request": "launch", + "mainClass": "org.myrobotlab.codec.RecorderPythonFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "CvData", + "request": "launch", + "mainClass": "org.myrobotlab.cv.CvData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WikipediaIndexer", + "request": "launch", + "mainClass": "org.myrobotlab.document.connector.WikipediaIndexer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Index", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Index", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MRLListener", + "request": "launch", + "mainClass": "org.myrobotlab.framework.MRLListener", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Message", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Message", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MethodCacheTest", + "request": "launch", + "mainClass": "org.myrobotlab.framework.MethodCacheTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NameGenerator", + "request": "launch", + "mainClass": "org.myrobotlab.framework.NameGenerator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Platform", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Platform", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Status", + "request": "launch", + "mainClass": "org.myrobotlab.framework.Status", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TypeConverter", + "request": "launch", + "mainClass": "org.myrobotlab.framework.TypeConverter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IvyWrapper", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.IvyWrapper", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MavenWrapper", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.MavenWrapper", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ServiceData", + "request": "launch", + "mainClass": "org.myrobotlab.framework.repo.ServiceData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "I2CFactory", + "request": "launch", + "mainClass": "org.myrobotlab.i2c.I2CFactory", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SerializableImage", + "request": "launch", + "mainClass": "org.myrobotlab.image.SerializableImage", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FileIO", + "request": "launch", + "mainClass": "org.myrobotlab.io.FileIO", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FindFile", + "request": "launch", + "mainClass": "org.myrobotlab.io.FindFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Zip", + "request": "launch", + "mainClass": "org.myrobotlab.io.Zip", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestPhysicsHingeJoint", + "request": "launch", + "mainClass": "org.myrobotlab.jme3.TestPhysicsHingeJoint", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestSimplePhysics", + "request": "launch", + "mainClass": "org.myrobotlab.jme3.TestSimplePhysics", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestJmeIMModel", + "request": "launch", + "mainClass": "org.myrobotlab.kinematics.TestJmeIMModel", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestJmeIntegratedMovement", + "request": "launch", + "mainClass": "org.myrobotlab.kinematics.TestJmeIntegratedMovement", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MapperLinearTest", + "request": "launch", + "mainClass": "org.myrobotlab.math.MapperLinearTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Node", + "request": "launch", + "mainClass": "org.myrobotlab.memory.Node", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Email", + "request": "launch", + "mainClass": "org.myrobotlab.net.Email", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Http", + "request": "launch", + "mainClass": "org.myrobotlab.net.Http", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HttpRequest", + "request": "launch", + "mainClass": "org.myrobotlab.net.HttpRequest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InstallCert", + "request": "launch", + "mainClass": "org.myrobotlab.net.InstallCert", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MjpegServer", + "request": "launch", + "mainClass": "org.myrobotlab.net.MjpegServer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NanoHTTPD", + "request": "launch", + "mainClass": "org.myrobotlab.net.NanoHTTPD", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TcpSerialHub", + "request": "launch", + "mainClass": "org.myrobotlab.net.TcpSerialHub", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TcpServer", + "request": "launch", + "mainClass": "org.myrobotlab.net.TcpServer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WsClient", + "request": "launch", + "mainClass": "org.myrobotlab.net.WsClient", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.oculus.OculusDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVData", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVData", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFaceRecognizer", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFaceRecognizer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterAddMask", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterAddMask", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterKinectFloorFinder", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterKinectFloorFinder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVFilterKinectPointCloud", + "request": "launch", + "mainClass": "org.myrobotlab.opencv.OpenCVFilterKinectPointCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InProcessCli", + "request": "launch", + "mainClass": "org.myrobotlab.process.InProcessCli", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Launcher", + "request": "launch", + "mainClass": "org.myrobotlab.process.Launcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RTTTLParser", + "request": "launch", + "mainClass": "org.myrobotlab.roomba.RTTTLParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RTTTLPlay", + "request": "launch", + "mainClass": "org.myrobotlab.roomba.RTTTLPlay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ProcParser", + "request": "launch", + "mainClass": "org.myrobotlab.runtime.ProcParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Adafruit16CServoDriver", + "request": "launch", + "mainClass": "org.myrobotlab.service.Adafruit16CServoDriver", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AdafruitIna219", + "request": "launch", + "mainClass": "org.myrobotlab.service.AdafruitIna219", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AdafruitMotorHat4Pi", + "request": "launch", + "mainClass": "org.myrobotlab.service.AdafruitMotorHat4Pi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ads1115", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ads1115", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Amt203Encoder", + "request": "launch", + "mainClass": "org.myrobotlab.service.Amt203Encoder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Android", + "request": "launch", + "mainClass": "org.myrobotlab.service.Android", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Arduino", + "request": "launch", + "mainClass": "org.myrobotlab.service.Arduino", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Arm", + "request": "launch", + "mainClass": "org.myrobotlab.service.Arm", + "projectName": "mrl" + }, + { + "type": "java", + "name": "As5048AEncoder", + "request": "launch", + "mainClass": "org.myrobotlab.service.As5048AEncoder", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AudioCapture", + "request": "launch", + "mainClass": "org.myrobotlab.service.AudioCapture", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AudioFile", + "request": "launch", + "mainClass": "org.myrobotlab.service.AudioFile", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AzureTranslator", + "request": "launch", + "mainClass": "org.myrobotlab.service.AzureTranslator", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BeagleBoardBlack", + "request": "launch", + "mainClass": "org.myrobotlab.service.BeagleBoardBlack", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Blender", + "request": "launch", + "mainClass": "org.myrobotlab.service.Blender", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Blocks", + "request": "launch", + "mainClass": "org.myrobotlab.service.Blocks", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Bno055", + "request": "launch", + "mainClass": "org.myrobotlab.service.Bno055", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BodyPart", + "request": "launch", + "mainClass": "org.myrobotlab.service.BodyPart", + "projectName": "mrl" + }, + { + "type": "java", + "name": "BoofCv", + "request": "launch", + "mainClass": "org.myrobotlab.service.BoofCv", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Chassis", + "request": "launch", + "mainClass": "org.myrobotlab.service.Chassis", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ChessGame", + "request": "launch", + "mainClass": "org.myrobotlab.service.ChessGame", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Clock", + "request": "launch", + "mainClass": "org.myrobotlab.service.Clock", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Cron", + "request": "launch", + "mainClass": "org.myrobotlab.service.Cron", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Database", + "request": "launch", + "mainClass": "org.myrobotlab.service.Database", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Deeplearning4j", + "request": "launch", + "mainClass": "org.myrobotlab.service.Deeplearning4j", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DiscordBot", + "request": "launch", + "mainClass": "org.myrobotlab.service.DiscordBot", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DiyServo", + "request": "launch", + "mainClass": "org.myrobotlab.service.DiyServo", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Docker", + "request": "launch", + "mainClass": "org.myrobotlab.service.Docker", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DocumentPipeline", + "request": "launch", + "mainClass": "org.myrobotlab.service.DocumentPipeline", + "projectName": "mrl" + }, + { + "type": "java", + "name": "DruppNeck", + "request": "launch", + "mainClass": "org.myrobotlab.service.DruppNeck", + "projectName": "mrl" + }, + { + "type": "java", + "name": "EddieControlBoard", + "request": "launch", + "mainClass": "org.myrobotlab.service.EddieControlBoard", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Elasticsearch", + "request": "launch", + "mainClass": "org.myrobotlab.service.Elasticsearch", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Emoji", + "request": "launch", + "mainClass": "org.myrobotlab.service.Emoji", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Esp8266", + "request": "launch", + "mainClass": "org.myrobotlab.service.Esp8266", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Esp8266_01", + "request": "launch", + "mainClass": "org.myrobotlab.service.Esp8266_01", + "projectName": "mrl" + }, + { + "type": "java", + "name": "FiniteStateMachine", + "request": "launch", + "mainClass": "org.myrobotlab.service.FiniteStateMachine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Git", + "request": "launch", + "mainClass": "org.myrobotlab.service.Git", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoPro", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoPro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleCloud", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleCloud", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleSearch", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleSearch", + "projectName": "mrl" + }, + { + "type": "java", + "name": "GoogleTranslate", + "request": "launch", + "mainClass": "org.myrobotlab.service.GoogleTranslate", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Gps", + "request": "launch", + "mainClass": "org.myrobotlab.service.Gps", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Gpt3", + "request": "launch", + "mainClass": "org.myrobotlab.service.Gpt3", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Hd44780", + "request": "launch", + "mainClass": "org.myrobotlab.service.Hd44780", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HtmlFilter", + "request": "launch", + "mainClass": "org.myrobotlab.service.HtmlFilter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HtmlParser", + "request": "launch", + "mainClass": "org.myrobotlab.service.HtmlParser", + "projectName": "mrl" + }, + { + "type": "java", + "name": "HttpClient", + "request": "launch", + "mainClass": "org.myrobotlab.service.HttpClient", + "projectName": "mrl" + }, + { + "type": "java", + "name": "I2cMux", + "request": "launch", + "mainClass": "org.myrobotlab.service.I2cMux", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IBus", + "request": "launch", + "mainClass": "org.myrobotlab.service.IBus", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ImageDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.service.ImageDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ImapEmailConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.ImapEmailConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Hand", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Hand", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Head", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Head", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InMoov2Torso", + "request": "launch", + "mainClass": "org.myrobotlab.service.InMoov2Torso", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IndianTts", + "request": "launch", + "mainClass": "org.myrobotlab.service.IndianTts", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IntegratedMovement", + "request": "launch", + "mainClass": "org.myrobotlab.service.IntegratedMovement", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Intro", + "request": "launch", + "mainClass": "org.myrobotlab.service.Intro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InverseKinematics", + "request": "launch", + "mainClass": "org.myrobotlab.service.InverseKinematics", + "projectName": "mrl" + }, + { + "type": "java", + "name": "InverseKinematics3D", + "request": "launch", + "mainClass": "org.myrobotlab.service.InverseKinematics3D", + "projectName": "mrl" + }, + { + "type": "java", + "name": "IpCamera", + "request": "launch", + "mainClass": "org.myrobotlab.service.IpCamera", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JFugue", + "request": "launch", + "mainClass": "org.myrobotlab.service.JFugue", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JMonkeyEngine", + "request": "launch", + "mainClass": "org.myrobotlab.service.JMonkeyEngine", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JMonkeyEngineTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.JMonkeyEngineTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "JavaScript", + "request": "launch", + "mainClass": "org.myrobotlab.service.JavaScript", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Joystick", + "request": "launch", + "mainClass": "org.myrobotlab.service.Joystick", + "projectName": "mrl" + }, + { + "type": "java", + "name": "KafkaConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.KafkaConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Keyboard", + "request": "launch", + "mainClass": "org.myrobotlab.service.Keyboard", + "projectName": "mrl" + }, + { + "type": "java", + "name": "KeyboardSim", + "request": "launch", + "mainClass": "org.myrobotlab.service.KeyboardSim", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LeapMotion", + "request": "launch", + "mainClass": "org.myrobotlab.service.LeapMotion", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LeapMotion2", + "request": "launch", + "mainClass": "org.myrobotlab.service.LeapMotion2", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lidar", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lidar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LidarVlp16", + "request": "launch", + "mainClass": "org.myrobotlab.service.LidarVlp16", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lloyd", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lloyd", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Lm75a", + "request": "launch", + "mainClass": "org.myrobotlab.service.Lm75a", + "projectName": "mrl" + }, + { + "type": "java", + "name": "LocalSpeech", + "request": "launch", + "mainClass": "org.myrobotlab.service.LocalSpeech", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Log", + "request": "launch", + "mainClass": "org.myrobotlab.service.Log", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mail", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mail", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MarySpeech", + "request": "launch", + "mainClass": "org.myrobotlab.service.MarySpeech", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Maven", + "request": "launch", + "mainClass": "org.myrobotlab.service.Maven", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MobilePlatform", + "request": "launch", + "mainClass": "org.myrobotlab.service.MobilePlatform", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Motor", + "request": "launch", + "mainClass": "org.myrobotlab.service.Motor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorDualPwm", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorDualPwm", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorHat4Pi", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorHat4Pi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MotorPort", + "request": "launch", + "mainClass": "org.myrobotlab.service.MotorPort", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MouseSim", + "request": "launch", + "mainClass": "org.myrobotlab.service.MouseSim", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MouthControl", + "request": "launch", + "mainClass": "org.myrobotlab.service.MouthControl", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mpr121", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mpr121", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mpu6050", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mpu6050", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Mqtt", + "request": "launch", + "mainClass": "org.myrobotlab.service.Mqtt", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MqttBroker", + "request": "launch", + "mainClass": "org.myrobotlab.service.MqttBroker", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MultiWii", + "request": "launch", + "mainClass": "org.myrobotlab.service.MultiWii", + "projectName": "mrl" + }, + { + "type": "java", + "name": "MyoThalmic", + "request": "launch", + "mainClass": "org.myrobotlab.service.MyoThalmic", + "projectName": "mrl" + }, + { + "type": "java", + "name": "NeoPixel", + "request": "launch", + "mainClass": "org.myrobotlab.service.NeoPixel", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OakD", + "request": "launch", + "mainClass": "org.myrobotlab.service.OakD", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusDiy", + "request": "launch", + "mainClass": "org.myrobotlab.service.OculusDiy", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OculusRift", + "request": "launch", + "mainClass": "org.myrobotlab.service.OculusRift", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OledSsd1306", + "request": "launch", + "mainClass": "org.myrobotlab.service.OledSsd1306", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCV", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenCV", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenCVTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenCVTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenNi", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenNi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "OpenWeatherMap", + "request": "launch", + "mainClass": "org.myrobotlab.service.OpenWeatherMap", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Osc", + "request": "launch", + "mainClass": "org.myrobotlab.service.Osc", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pcf8574", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pcf8574", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pid", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pid", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pingdar", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pingdar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pir", + "request": "launch", + "mainClass": "org.myrobotlab.service.Pir", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Polly", + "request": "launch", + "mainClass": "org.myrobotlab.service.Polly", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ProgramAB", + "request": "launch", + "mainClass": "org.myrobotlab.service.ProgramAB", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Py4j", + "request": "launch", + "mainClass": "org.myrobotlab.service.Py4j", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Python", + "request": "launch", + "mainClass": "org.myrobotlab.service.Python", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RSSConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.RSSConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Random", + "request": "launch", + "mainClass": "org.myrobotlab.service.Random", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RasPi", + "request": "launch", + "mainClass": "org.myrobotlab.service.RasPi", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Rekognition", + "request": "launch", + "mainClass": "org.myrobotlab.service.Rekognition", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Relay", + "request": "launch", + "mainClass": "org.myrobotlab.service.Relay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "RoboClaw", + "request": "launch", + "mainClass": "org.myrobotlab.service.RoboClaw", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Roomba", + "request": "launch", + "mainClass": "org.myrobotlab.service.Roomba", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ros", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ros", + "projectName": "mrl" + }, { "type": "java", "name": "Runtime", "request": "launch", "mainClass": "org.myrobotlab.service.Runtime", - "projectName": "mrl", - "args": [ - "--log-level", - "info", - "-s", - "webgui", "WebGui", "intro", "Intro", "python", "Python", - "-c", - "dev" - ] + "projectName": "mrl" + }, + { + "type": "java", + "name": "Sabertooth", + "request": "launch", + "mainClass": "org.myrobotlab.service.Sabertooth", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Security", + "request": "launch", + "mainClass": "org.myrobotlab.service.Security", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SegmentDisplay", + "request": "launch", + "mainClass": "org.myrobotlab.service.SegmentDisplay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SensorMonitor", + "request": "launch", + "mainClass": "org.myrobotlab.service.SensorMonitor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Serial", + "request": "launch", + "mainClass": "org.myrobotlab.service.Serial", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SerialRelay", + "request": "launch", + "mainClass": "org.myrobotlab.service.SerialRelay", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Servo", + "request": "launch", + "mainClass": "org.myrobotlab.service.Servo", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ServoMixer", + "request": "launch", + "mainClass": "org.myrobotlab.service.ServoMixer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Shoutbox", + "request": "launch", + "mainClass": "org.myrobotlab.service.Shoutbox", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SlackBot", + "request": "launch", + "mainClass": "org.myrobotlab.service.SlackBot", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Solr", + "request": "launch", + "mainClass": "org.myrobotlab.service.Solr", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Sphinx", + "request": "launch", + "mainClass": "org.myrobotlab.service.Sphinx", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SpotMicro", + "request": "launch", + "mainClass": "org.myrobotlab.service.SpotMicro", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Ssc32UsbServoController", + "request": "launch", + "mainClass": "org.myrobotlab.service.Ssc32UsbServoController", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TarsosDsp", + "request": "launch", + "mainClass": "org.myrobotlab.service.TarsosDsp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Tensorflow", + "request": "launch", + "mainClass": "org.myrobotlab.service.Tensorflow", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TesseractOcr", + "request": "launch", + "mainClass": "org.myrobotlab.service.TesseractOcr", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Test", + "request": "launch", + "mainClass": "org.myrobotlab.service.Test", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TestCatcher", + "request": "launch", + "mainClass": "org.myrobotlab.service.TestCatcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "ThingSpeak", + "request": "launch", + "mainClass": "org.myrobotlab.service.ThingSpeak", + "projectName": "mrl" + }, + { + "type": "java", + "name": "TopCodes", + "request": "launch", + "mainClass": "org.myrobotlab.service.TopCodes", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Tracking", + "request": "launch", + "mainClass": "org.myrobotlab.service.Tracking", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Twitter", + "request": "launch", + "mainClass": "org.myrobotlab.service.Twitter", + "projectName": "mrl" + }, + { + "type": "java", + "name": "UltrasonicSensor", + "request": "launch", + "mainClass": "org.myrobotlab.service.UltrasonicSensor", + "projectName": "mrl" + }, + { + "type": "java", + "name": "UltrasonicSensorTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.UltrasonicSensorTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Updater", + "request": "launch", + "mainClass": "org.myrobotlab.service.Updater", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VideoStreamer", + "request": "launch", + "mainClass": "org.myrobotlab.service.VideoStreamer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VirtualArduino", + "request": "launch", + "mainClass": "org.myrobotlab.service.VirtualArduino", + "projectName": "mrl" + }, + { + "type": "java", + "name": "VoiceRss", + "request": "launch", + "mainClass": "org.myrobotlab.service.VoiceRss", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WatchDogTimer", + "request": "launch", + "mainClass": "org.myrobotlab.service.WatchDogTimer", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebGui", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebGui", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebSocketConnector", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebSocketConnector", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Webcam", + "request": "launch", + "mainClass": "org.myrobotlab.service.Webcam", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebkitSpeechRecognition", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebkitSpeechRecognition", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WebkitSpeechSynthesis", + "request": "launch", + "mainClass": "org.myrobotlab.service.WebkitSpeechSynthesis", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Wii", + "request": "launch", + "mainClass": "org.myrobotlab.service.Wii", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WiiDar", + "request": "launch", + "mainClass": "org.myrobotlab.service.WiiDar", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WikiDataFetcher", + "request": "launch", + "mainClass": "org.myrobotlab.service.WikiDataFetcher", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Wikipedia", + "request": "launch", + "mainClass": "org.myrobotlab.service.Wikipedia", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WolframAlpha", + "request": "launch", + "mainClass": "org.myrobotlab.service.WolframAlpha", + "projectName": "mrl" + }, + { + "type": "java", + "name": "WorkE", + "request": "launch", + "mainClass": "org.myrobotlab.service.WorkE", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Xmpp", + "request": "launch", + "mainClass": "org.myrobotlab.service.Xmpp", + "projectName": "mrl" + }, + { + "type": "java", + "name": "YahooFinanceStockQuote", + "request": "launch", + "mainClass": "org.myrobotlab.service.YahooFinanceStockQuote", + "projectName": "mrl" + }, + { + "type": "java", + "name": "_TemplateService", + "request": "launch", + "mainClass": "org.myrobotlab.service._TemplateService", + "projectName": "mrl" + }, + { + "type": "java", + "name": "Pin", + "request": "launch", + "mainClass": "org.myrobotlab.service.data.Pin", + "projectName": "mrl" + }, + { + "type": "java", + "name": "SpeechSynthesisTest", + "request": "launch", + "mainClass": "org.myrobotlab.service.interfaces.SpeechSynthesisTest", + "projectName": "mrl" + }, + { + "type": "java", + "name": "StringUtil", + "request": "launch", + "mainClass": "org.myrobotlab.string.StringUtil", + "projectName": "mrl" + }, + { + "type": "java", + "name": "AbstractTest", + "request": "launch", + "mainClass": "org.myrobotlab.test.AbstractTest", + "projectName": "mrl" } ] } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java index 6fd3e43b13..03b8c251b9 100644 --- a/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java +++ b/src/main/java/org/myrobotlab/kinematics/DHRobotArm.java @@ -235,10 +235,7 @@ public boolean moveToGoal(Point goal) { int numSteps = 0; double iterStep = 0.05; // we're in millimeters.. - double errorThreshold = 20.0; - - maxIterations = 1000; - + double errorThreshold = 2.0; // what's the current point while (true) { numSteps++; From b521675fed268c3c22f1cd7a6cd09e921d65fb33 Mon Sep 17 00:00:00 2001 From: grog Date: Sat, 11 Nov 2023 08:48:50 -0800 Subject: [PATCH 081/232] random service update --- .../org/myrobotlab/service/JMonkeyEngine.java | 2 +- .../java/org/myrobotlab/service/Random.java | 262 ++++++++++-------- .../myrobotlab/service/meta/RandomMeta.java | 5 +- src/main/resources/resource/Random/Random.py | 54 +++- .../WebGui/app/service/views/RandomGui.html | 12 +- .../org/myrobotlab/service/RandomTest.java | 18 +- 6 files changed, 212 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java index fbc5ef5e99..a22156778e 100644 --- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java +++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java @@ -2157,7 +2157,7 @@ public void simpleInitApp() { new File(getDataDir()).mkdirs(); new File(getResourceDir()).mkdirs(); - // assetManager.registerLocator("./", FileLocator.class); + assetManager.registerLocator("./", FileLocator.class); assetManager.registerLocator(getDataDir(), FileLocator.class); assetManager.registerLocator(assetsDir, FileLocator.class); assetManager.registerLocator(modelsDir, FileLocator.class); diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java index dca027aa3a..71ca99616f 100644 --- a/src/main/java/org/myrobotlab/service/Random.java +++ b/src/main/java/org/myrobotlab/service/Random.java @@ -32,6 +32,10 @@ public class Random extends Service { public final static Logger log = LoggerFactory.getLogger(Random.class); + transient private RandomProcessor processor = null; + + transient private final Object lock = new Object(); + /** * * RandomMessage is used to contain the ranges of values and intervals for @@ -39,14 +43,15 @@ public class Random extends Service { * */ static public class RandomMessage { + public String taskName; public String name; public String method; public Range[] data; public boolean enabled = true; public long minIntervalMs; public long maxIntervalMs; - public long interval; public boolean oneShot = false; + public transient long nextProcessTimeTs = 0; public RandomMessage() { } @@ -103,6 +108,23 @@ public double getRandom(double min, double max) { return min + (Math.random() * (max - min)); } + public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, String name, String method, Integer... values) { + addRandom(taskName, minIntervalMs, maxIntervalMs, name, method, toRanges((Object[]) values)); + } + + public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, String name, String method, Double... values) { + addRandom(taskName, minIntervalMs, maxIntervalMs, name, method, toRanges((Object[]) values)); + } + + // FIXME - test this + public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, String name, String method) { + addRandom(taskName, minIntervalMs, maxIntervalMs, name, method, toRanges((Object[]) null)); + } + + public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, String name, String method, String... params) { + addRandom(taskName, minIntervalMs, maxIntervalMs, name, method, setRange((Object[]) params)); + } + public void addRandom(long minIntervalMs, long maxIntervalMs, String name, String method, Integer... values) { addRandom(minIntervalMs, maxIntervalMs, name, method, toRanges((Object[]) values)); } @@ -170,74 +192,105 @@ public void removeAll() { } public void addRandom(long minIntervalMs, long maxIntervalMs, String name, String method, Range... ranges) { + String taskName = String.format("%s.%s", name, method); + addRandom(taskName, minIntervalMs, maxIntervalMs, name, method, ranges); + } - RandomMessage msg = new RandomMessage(); - msg.name = name; - msg.method = method; - msg.minIntervalMs = minIntervalMs; - msg.maxIntervalMs = maxIntervalMs; - msg.data = ranges; - - String key = String.format("%s.%s", name, method); - randomData.put(key, msg); - - msg.interval = getRandom(minIntervalMs, maxIntervalMs); - log.info("add random message {} in {} ms", key, msg.interval); - if (enabled) { - // only if global enabled is enabled do we start the task - addTask(key, 0, msg.interval, "process", key); - } + public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, String name, String method, Range... ranges) { + + RandomMessage data = new RandomMessage(); + data.name = name; + data.method = method; + data.minIntervalMs = minIntervalMs; + data.maxIntervalMs = maxIntervalMs; + data.data = ranges; + data.enabled = true; + + randomData.put(taskName, data); + + log.info("add random message {} in {} to {} ms", taskName, data.minIntervalMs, data.maxIntervalMs); broadcastState(); } - public void process(String key) { - // if (!enabled) { - // return; - // } + private class RandomProcessor extends Thread { - RandomMessage msg = randomData.get(key); - if (msg == null || !msg.enabled) { - return; + public RandomProcessor(String name) { + super(name); } - Message m = Message.createMessage(getName(), msg.name, msg.method, null); - if (msg.data != null) { - List data = new ArrayList<>(); - - for (int i = 0; i < msg.data.length; ++i) { - Object o = msg.data[i]; - if (o instanceof Range) { - Range range = (Range) o; - Object param = null; - - if (range.set != null) { - int rand = getRandom(0, range.set.size() - 1); - param = range.set.get(rand); - } else if (range.min instanceof Double) { - param = getRandom((Double) range.min, (Double) range.max); - } else if (range.min instanceof Long) { - param = getRandom((Long) range.min, (Long) range.max); - } else if (range.min instanceof Integer) { - param = getRandom((Integer) range.min, (Integer) range.max); + public void run() { + while (enabled) { + try { + // minimal interval time for processor to check + // and see if any random event needs processing + + sleep(200); + for (String key : randomData.keySet()) { + + long now = System.currentTimeMillis(); + + RandomMessage randomEntry = randomData.get(key); + if (!randomEntry.enabled) { + continue; + } + + // first time set + if (randomEntry.nextProcessTimeTs == 0) { + randomEntry.nextProcessTimeTs = now + getRandom((Long) randomEntry.minIntervalMs, (Long) randomEntry.maxIntervalMs); + } + + if (now < randomEntry.nextProcessTimeTs) { + // this entry isn't ready + continue; + } + + Message m = Message.createMessage(getName(), randomEntry.name, randomEntry.method, null); + if (randomEntry.data != null) { + List data = new ArrayList<>(); + + for (int i = 0; i < randomEntry.data.length; ++i) { + Object o = randomEntry.data[i]; + if (o instanceof Range) { + Range range = (Range) o; + Object param = null; + + if (range.set != null) { + int rand = getRandom(0, range.set.size() - 1); + param = range.set.get(rand); + } else if (range.min instanceof Double) { + param = getRandom((Double) range.min, (Double) range.max); + } else if (range.min instanceof Long) { + param = getRandom((Long) range.min, (Long) range.max); + } else if (range.min instanceof Integer) { + param = getRandom((Integer) range.min, (Integer) range.max); + } + + data.add(param); + } + } + m.data = data.toArray(); + } + m.sendingMethod = "process"; + log.debug("random msg @ {} ms {}", now - randomEntry.nextProcessTimeTs, m); + out(m); + + // auto-disable oneshot + if (randomEntry.oneShot) { + randomEntry.enabled = false; + } + + // reset next processing time + randomEntry.nextProcessTimeTs = now + getRandom((Long) randomEntry.minIntervalMs, (Long) randomEntry.maxIntervalMs); + } - data.add(param); + } catch (Exception e) { + error(e); } - } - m.data = data.toArray(); - } - m.sendingMethod = "process"; - log.info("random msg @ {} ms {}", msg.interval, m); - out(m); - - purgeTask(key); - if (!msg.oneShot) { - msg.interval = getRandom(msg.minIntervalMs, msg.maxIntervalMs); - // must re-schedule unless one shot - if (enabled) { - // only if global enabled is enabled do we start the task - addTask(key, 0, msg.interval, "process", key); - } + + } // while (enabled) { + + log.info("Random {}-processor terminating", getName()); } } @@ -280,12 +333,12 @@ public RandomConfig apply(RandomConfig c) { return c; } + @Deprecated /* use remove(String key) */ public RandomMessage remove(String name, String method) { return remove(String.format("%s.%s", name, method)); } public RandomMessage remove(String key) { - purgeTask(key); return randomData.remove(key); } @@ -294,79 +347,49 @@ public Set getKeySet() { } public void disable(String key) { - // exact match - if (key.contains(".")) { - RandomMessage msg = randomData.get(key); - if (msg == null) { - log.warn("cannot disable random event with key {}", key); - return; - } - randomData.get(key).enabled = false; - purgeTask(key); + + if (!randomData.containsKey(key)) { + error("disable cannot find key %s", key); return; } - // must be name - disable "all" for this service - for (RandomMessage msg : randomData.values()) { - if (msg.name.equals(key)) { - msg.enabled = false; - purgeTask(String.format("%s.%s", msg.name, msg.method)); - } - } + + randomData.get(key).enabled = false; } public void enable(String key) { - // exact match - if (key.contains(".")) { - RandomMessage msg = randomData.get(key); - if (msg == null) { - log.warn("cannot enable random event with key {}", key); - return; - } - randomData.get(key).enabled = true; - if (enabled) { - // only if global enabled is enabled do we start the task - addTask(key, 0, msg.interval, "process", key); - } + if (!randomData.containsKey(key)) { + error("disable cannot find key %s", key); return; } - // must be name - disable "all" for this service - String name = key; - for (RandomMessage msg : randomData.values()) { - if (msg.name.equals(name)) { - msg.enabled = true; - String fullKey = String.format("%s.%s", msg.name, msg.method); - if (enabled) { - // only if global enabled is enabled do we start the task - addTask(fullKey, 0, msg.interval, "process", fullKey); - } - } - } + randomData.get(key).enabled = true; } public void disable() { - // remove all timed attempts of processing random - // events - purgeTasks(); - enabled = false; - broadcastState(); + synchronized (lock) { + enabled = false; + processor = null; + broadcastState(); + } } public void enable() { - for (RandomMessage msg : randomData.values()) { - // re-enable tasks which were previously enabled - if (msg.enabled == true) { - String fullKey = String.format("%s.%s", msg.name, msg.method); - addTask(fullKey, 0, msg.interval, "process", fullKey); + synchronized (lock) { + enabled = true; + if (processor == null) { + processor = new RandomProcessor(String.format("%s-processor", getName())); + processor.start(); + // wait until thread starts + sleep(200); + } else { + info("%s already enabled"); } + broadcastState(); } - - enabled = true; - broadcastState(); } public void purge() { randomData.clear(); - purgeTasks(); + broadcastState(); } public Set getMethodsFromName(String serviceName) { @@ -395,13 +418,22 @@ public List methodQuery(String serviceName, String methodName) { } return MethodCache.getInstance().query(si.getClass().getCanonicalName(), methodName); } + + public Map getRandomEvents(){ + return randomData; + } + + public RandomMessage getRandomEvent(String key) { + return randomData.get(key); + } public static void main(String[] args) { try { LoggingFactory.init(Level.INFO); - + Runtime.setConfig("dev"); Runtime.start("c1", "Clock"); + Runtime.start("python", "Python"); Random random = (Random) Runtime.start("random", "Random"); diff --git a/src/main/java/org/myrobotlab/service/meta/RandomMeta.java b/src/main/java/org/myrobotlab/service/meta/RandomMeta.java index c18ebfa9a7..f7765982c2 100644 --- a/src/main/java/org/myrobotlab/service/meta/RandomMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/RandomMeta.java @@ -17,13 +17,10 @@ public RandomMeta() { // add a cool description addDescription("provides a service for random message generation"); - // false will prevent it being seen in the ui - setAvailable(true); - // add dependencies if necessary // addDependency("com.twelvemonkeys.common", "common-lang", "3.1.1"); - setAvailable(false); + setAvailable(true); // add it to one or many categories addCategory("general"); diff --git a/src/main/resources/resource/Random/Random.py b/src/main/resources/resource/Random/Random.py index 7d5fa86f9b..84c79ab17a 100644 --- a/src/main/resources/resource/Random/Random.py +++ b/src/main/resources/resource/Random/Random.py @@ -7,34 +7,63 @@ ######################################### # start the service -python = runtime.start("python","Python") -random = runtime.start("random","Random") -clock = runtime.start("clock","Clock") +python = runtime.start("python", "Python") +random = runtime.start("random", "Random") +clock = runtime.start("clock", "Clock") + + +def happy(): + print("i am happy") + + +def sad(): + print("i am sad") + + +def angry(): + print("i am angry") + + +# add a named random task +random.addRandom("random emotion", 1000, 2000, "python", "exec", "happy()", "sad()", "angry()") + # enable random events random.enable() -# roll the dice every 1 to 2 seonds -random.addRandom(1000, 2000, "python", "roll_dice", random.intRange(1, 6)) + + def roll_dice(value): print("roll_dice " + str(value)) + +# roll the dice every 1 to 2 seonds +random.addRandom(1000, 2000, "python", "roll_dice", random.intRange(1, 6)) + + # add a complex dice -random.addRandom(1000, 2000, "python", "roll_complex_dice", random.doubleRange(1, 6)) def roll_complex_dice(value): print("roll_complex_dice " + str(value)) -# roll the dice every 1 to 2 seonds -random.addRandom(1000, 2000, "python", "random_color", random.setRange("red","green","blue","yellow")) +# roll the complex dice every 1 to 2 seonds +random.addRandom(1000, 2000, "python", "roll_complex_dice", random.doubleRange(1, 6)) + + def random_color(value): print("random_color " + str(value)) +# roll the dice every 1 to 2 seonds +random.addRandom(1000, 2000, "python", "random_color", random.setRange("red", "green", "blue", "yellow")) + + # do a complex multi parameter, multi-type method -random.addRandom(1000, 2000, "python", "kitchen_sink", random.intRange(1, 6), random.doubleRange(1, 6), random.setRange("red","green","blue","yellow"), random.setRange("bob","jane","fred","mary")) def kitchen_sink(dice, complex_dice, colors, names): print("kitchen_sink ", dice, complex_dice, colors, names) + +random.addRandom(1000, 2000, "python", "kitchen_sink", random.intRange(1, 6), random.doubleRange(1, 6), random.setRange("red","green","blue","yellow"), random.setRange("bob","jane","fred","mary")) + # set the interval on a clock between 1000 and 8000 # if you look in the UI you can see the clock interval changing random.addRandom(200, 500, "clock", "setInterval", random.intRange(1000, 8000)) @@ -43,13 +72,8 @@ def kitchen_sink(dice, complex_dice, colors, names): random.addRandom(200, 500, "clock", "startClock") random.addRandom(200, 500, "clock", "stopClock") - -# run it all for 8 seconds -sleep(8) - # disable single random event generator - must be explicit with name.method key random.disable("python.roll_dice") -sleep(8) # you know longer should see the python.roll_dice event firing - since it was explicitly disabled @@ -61,4 +85,4 @@ def kitchen_sink(dice, complex_dice, colors, names): # random.enable() # stop events and clear all random event ata -# random.purge() \ No newline at end of file +# random.purge() diff --git a/src/main/resources/resource/WebGui/app/service/views/RandomGui.html b/src/main/resources/resource/WebGui/app/service/views/RandomGui.html index 34c57ea2ca..b49a6365a0 100644 --- a/src/main/resources/resource/WebGui/app/service/views/RandomGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/RandomGui.html @@ -12,10 +12,16 @@

    ( - + [ + {{param.min}} - {{param.max}} - {{item}} - + + + {{item}} + , + ] + , + ) every {{value.minIntervalMs}} ms to {{value.maxIntervalMs}} ms diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java index 450ffa04da..cf0d85f94e 100644 --- a/src/test/java/org/myrobotlab/service/RandomTest.java +++ b/src/test/java/org/myrobotlab/service/RandomTest.java @@ -3,7 +3,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.util.Map; + import org.myrobotlab.framework.Service; +import org.myrobotlab.service.Random.RandomMessage; public class RandomTest extends AbstractServiceTest { @@ -25,8 +28,9 @@ public void testService() throws Exception { assertTrue("set interval 1000 base value", 1000 == clock.getInterval()); random.addRandom(0, 200, "clock", "setInterval", 5000, 10000); + random.enable(); - sleep(200); + sleep(300); assertTrue("should have method", random.getKeySet().contains("clock.setInterval")); @@ -45,13 +49,13 @@ public void testService() throws Exception { assertTrue("clock should be started 1", clock.isClockRunning()); // disable all of a services random events - random.disable("clock"); + random.disable("clock.startClock"); clock.stopClock(); sleep(200); assertTrue("clock should not be started", !clock.isClockRunning()); // enable all of a service's random events - random.enable("clock"); + random.enable("clock.startClock"); sleep(200); assertTrue("clock should be started 2", clock.isClockRunning()); @@ -77,7 +81,15 @@ public void testService() throws Exception { assertTrue("clock should not be started", !clock.isClockRunning()); assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval()); assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000); + + clock.stopClock(); + random.purge(); + Map events = random.getRandomEvents(); + assertTrue(events.size() == 0); + + random.addRandom("named task", 200, 500, "clock", "setInterval", 100, 1000, 10); + clock.releaseService(); random.releaseService(); From 9431841bf443969778f4b9efcd47ff64fea290b1 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 16:11:53 -0800 Subject: [PATCH 082/232] gpt3 prefix and websockethandler fix --- src/main/java/org/myrobotlab/service/Gpt3.java | 6 +++++- .../java/org/myrobotlab/service/config/Gpt3Config.java | 5 +++++ src/main/java/org/myrobotlab/vertx/WebSocketHandler.java | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/Gpt3.java b/src/main/java/org/myrobotlab/service/Gpt3.java index 67ed717639..82d9ad90d2 100644 --- a/src/main/java/org/myrobotlab/service/Gpt3.java +++ b/src/main/java/org/myrobotlab/service/Gpt3.java @@ -61,7 +61,7 @@ public class Gpt3 extends Service implements TextListener, TextPubli private String currentChannelName; private String currentChannelType; - + public Gpt3(String n, String id) { super(n, id); } @@ -85,6 +85,10 @@ public Response getResponse(String text) { sleep(); responseText = "Ok, I will go to sleep"; } + + if (c.prefix != null) { + text = c.prefix + " " + text; + } if (!c.sleeping) { diff --git a/src/main/java/org/myrobotlab/service/config/Gpt3Config.java b/src/main/java/org/myrobotlab/service/config/Gpt3Config.java index 62a6627a1d..48f3612f17 100644 --- a/src/main/java/org/myrobotlab/service/config/Gpt3Config.java +++ b/src/main/java/org/myrobotlab/service/config/Gpt3Config.java @@ -18,6 +18,11 @@ public class Gpt3Config extends ServiceConfig { public String engine = "gpt-3.5-turbo"; // "text-davinci-003" public String wakeWord = "wake"; public String sleepWord = "sleep"; + /** + * static prefix to send to gpt3 + * e.g. " talk like a pirate when responding, " + */ + public String prefix = null; @Override public Plan getDefault(Plan plan, String name) { diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java index e0ad0f2e61..d8b07023f2 100644 --- a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java +++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java @@ -108,6 +108,14 @@ public void handle(ServerWebSocket socket) { // client MultiMap headers = socket.headers(); String uri = socket.uri(); + + // 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()); From 4a891ac073e0a96c8855fe6977214108afab5be5 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 17:28:33 -0800 Subject: [PATCH 083/232] updates --- .../java/org/myrobotlab/service/InMoov2.java | 106 +++++------------- .../java/org/myrobotlab/service/NeoPixel.java | 12 +- .../service/config/InMoov2Config.java | 2 +- .../service/config/NeoPixelConfig.java | 36 ++++++ .../service/data/LedDisplayData.java | 8 +- 5 files changed, 80 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 59ad222ab1..2e328bdf87 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -15,6 +15,7 @@ import org.apache.commons.io.FilenameUtils; import org.myrobotlab.framework.Message; +import org.myrobotlab.framework.Plan; import org.myrobotlab.framework.Platform; import org.myrobotlab.framework.Registration; import org.myrobotlab.framework.Service; @@ -35,7 +36,6 @@ import org.myrobotlab.service.config.OpenCVConfig; import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; -import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; import org.myrobotlab.service.interfaces.IKJointAngleListener; import org.myrobotlab.service.interfaces.JoystickListener; @@ -106,8 +106,6 @@ public static boolean loadFile(String file) { */ protected String bootedConfig = null; - protected LedDisplayData led = new LedDisplayData(); - protected transient ProgramAB chatBot; protected List configList; @@ -159,8 +157,6 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected Map ledDisplayMap = new TreeMap<>(); - /** * supported locales */ @@ -200,14 +196,6 @@ public InMoov2(String n, String id) { stateDefaults.add("powerDown"); // stops heartbeat, listening ? stateDefaults.add("shutdown");// ends mrl - ledDisplayMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); - ledDisplayMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); - ledDisplayMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); - ledDisplayMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); - ledDisplayMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); - ledDisplayMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); - ledDisplayMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); - customSoundMap.put("boot", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/confirmation.wav")); customSoundMap.put("wake", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/ting.wav")); customSoundMap.put("firstInit", FileIO.gluePaths(getResourceDir(), "system/sounds/Notifications/select.wav")); @@ -511,14 +499,6 @@ public void firstInit() { } } - public void flash(String name) { - LedDisplayData led = ledDisplayMap.get(name); - if (led == null) { - led = ledDisplayMap.get("default"); - } - invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); - } - /** * used to configure a flashing event - could use configuration to signal * different colors and states @@ -526,18 +506,12 @@ public void flash(String name) { * @return */ public void flash() { - if (ledDisplayMap.get("default") != null) { - LedDisplayData led = ledDisplayMap.get("default"); - invoke("publishFlash", led.red, led.green, led.blue, led.count, led.timeOn, led.timeOff); - } + invoke("publishFlash", "default"); } - public void flash(int r, int g, int b, int count) { - // FIXME - this should be checking a protected "state" - if (ledDisplayMap.get("default") != null) { - LedDisplayData led = ledDisplayMap.get("default"); - invoke("publishFlash", r, g, b, count, led.timeOn, led.timeOff); - } + public String flash(String name) { + invoke("publishFlash", name); + return name; } public void fullSpeed() { @@ -1074,6 +1048,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) { public void onConfigFinished(String configName) { log.info("onConfigFinished"); + configStarted = false; invoke("publishBoot"); } @@ -1161,11 +1136,10 @@ public void onHeartbeat() { // flash error until errors are cleared if (config.healthCheckFlash) { - if (errors.size() > 0 && ledDisplayMap.containsKey("error")) { - invoke("publishFlash", ledDisplayMap.get("error")); - } else if (ledDisplayMap.containsKey("heartbeat")) { - LedDisplayData heartbeat = ledDisplayMap.get("heartbeat"); - invoke("publishFlash", heartbeat); + if (errors.size() > 0) { + invoke("publishFlash", "error"); + } else { + invoke("publishFlash", "heartbeat"); } } @@ -1289,21 +1263,9 @@ public void onPeak(double volume) { } } - /** - * onPeak volume callback TODO - maybe make it variable with volume ? - * - * @param volume - */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.timeOn = 500; - led.timeOff = 10; - // FIXME flash on config.flashOnBoot - invoke("publishFlash"); + + invoke("publishFlash", "pirOn"); ProgramAB chatBot = (ProgramAB)getPeer("chatBot"); if (chatBot != null) { String botState = chatBot.getPredicate("botState"); @@ -1531,19 +1493,24 @@ public List publishConfigList() { return configList; } - public LedDisplayData publishFlash(int r, int g, int b, int count, long timeOn, long timeOff) { - LedDisplayData data = new LedDisplayData(); - data.red = r; - data.green = g; - data.blue = b; - data.count = count; - data.timeOn = timeOn; - data.timeOff = timeOff; - return data; + /** + * publishes a name for NeoPixel.onFlash to consume + * @param name + * @return + */ + public String publishFlash(String name) { + return name; } - - public LedDisplayData publishFlash(LedDisplayData data) { - return data; + + /** + * publishes a name for NeoPixel.onFlash to consume, + * in a seperate channel to potentially be used by + * "speaking only" leds + * @param name + * @return + */ + public String publishSpeakingFlash(String name) { + return name; } /** @@ -1555,16 +1522,6 @@ public void publishInactivity() { fsm.fire("inactvity"); } - /** - * used to configure a flashing event - could use configuration to signal - * different colors and states - * - * @return - */ - public LedDisplayData publishFlash() { - return led; - } - /** * A more extensible interface point than publishEvent FIXME - create * interface for this @@ -2345,12 +2302,7 @@ public void closeHands() { closeLeftHand(); closeRightHand(); } - - public Event onEvent(Event event) { - return event; - } - public void wake() { log.info("wake"); // do waking things - based on config diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 8e7b84eafa..780d0a15b4 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -422,9 +422,15 @@ public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { data.timeOff = timeOff; displayQueue.add(data); } - - public void onFlash(LedDisplayData data) { - displayQueue.add(data); + + /** + * Publishes a flash based on a predefined name + * @param name + */ + public void onFlash(String name) { + if (config.flashMap != null && config.flashMap.containsKey(name)) { + displayQueue.add(config.flashMap.get(name)); + } } public void flashBrightness(double brightNess) { diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index c4c69f3cb9..227d20e5d3 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -523,7 +523,7 @@ public Plan getDefault(Plan plan, String name) { // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); // InMoov2 --to--> service - listeners.add(new Listener("publishFlash", getPeerName("neoPixel"), "onLedDisplay")); + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"))); listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); diff --git a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java index e7fbb17cbe..7f012d7e38 100644 --- a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java +++ b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java @@ -1,5 +1,11 @@ package org.myrobotlab.service.config; +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.framework.Plan; +import org.myrobotlab.service.data.LedDisplayData; + public class NeoPixelConfig extends ServiceConfig { public Integer pin = null; @@ -16,5 +22,35 @@ public class NeoPixelConfig extends ServiceConfig { // auto clears flashes public boolean autoClear = false; public int idleTimeout = 1000; + + /** + * Map of predefined led flashes, defined here in configuration. + * Another service simply needs to publishFlash(name) and the + * neopixel will get the defined flash data if defined and process + * it. + */ + public Map flashMap = new HashMap<>(); + + + /** + * reason why we initialize default for flashMap here, is so + * we don't need to do a data copy over to a service's member variable + */ + public Plan getDefault(Plan plan, String name) { + super.getDefault(plan, name); + + flashMap.put("error", new LedDisplayData(120, 0, 0, 3, 30, 30)); + flashMap.put("info", new LedDisplayData(0, 0, 120, 1, 30, 30)); + flashMap.put("success", new LedDisplayData(0, 0, 120, 2, 30, 30)); + flashMap.put("warn", new LedDisplayData(100, 100, 0, 3, 30, 30)); + flashMap.put("heartbeat", new LedDisplayData(210, 110, 0, 2, 100, 30)); + flashMap.put("pirOn", new LedDisplayData(60, 200, 90, 3, 100, 30)); + flashMap.put("onPeakColor", new LedDisplayData(180, 53, 21, 3, 60, 30)); + flashMap.put("speaking", new LedDisplayData(0, 183, 90, 2, 60, 30)); + + return plan; + } + } + diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java index 3658782913..b46ab5e862 100644 --- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java +++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java @@ -12,11 +12,13 @@ public class LedDisplayData { public String action; // fill | flash | play animation | stop | clear - public int red; + public int red = 0; - public int green; + public int green = 0; - public int blue; + public int blue = 0; + + // public int brightness = 255; // public int white?; From 3966bdffd72e61271b1c3e7198a55e3dc10a57b0 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 19:10:14 -0800 Subject: [PATCH 084/232] removed invoker --- .../framework/interfaces/JsonInvoker.java | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java diff --git a/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java b/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java deleted file mode 100644 index 76b4628079..0000000000 --- a/src/main/java/org/myrobotlab/framework/interfaces/JsonInvoker.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.myrobotlab.framework.interfaces; - -import org.myrobotlab.framework.Message; - -public interface JsonInvoker { - - /** - * No parameter method - * @param method - * @return - */ - public Object invoke(String method); - - /** - * Encoded parameters as a JSON String (encoded once!) - * @param method - * @param encodedParameters - * @return - */ - public Object invoke(String method, String encodedParameters); - -} From 40c36babec51dfc1b3344ec99e5d289b25befe4c Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 19:11:44 -0800 Subject: [PATCH 085/232] reverting to develop branch webgui.main --- src/main/java/org/myrobotlab/service/WebGui.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java index d007befe6e..dd2178bf8c 100644 --- a/src/main/java/org/myrobotlab/service/WebGui.java +++ b/src/main/java/org/myrobotlab/service/WebGui.java @@ -1179,7 +1179,7 @@ public static void main(String[] args) { // Platform.setVirtual(true); - Runtime.startConfig("dev"); + Runtime.startConfig("default"); boolean done = true; if (done) { From 70e5a4dd67ff13209f92d1783e6bf6a85b9d6962 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 19:13:51 -0800 Subject: [PATCH 086/232] reverting webgui.main --- src/main/java/org/myrobotlab/service/WebGui.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java index dd2178bf8c..d007befe6e 100644 --- a/src/main/java/org/myrobotlab/service/WebGui.java +++ b/src/main/java/org/myrobotlab/service/WebGui.java @@ -1179,7 +1179,7 @@ public static void main(String[] args) { // Platform.setVirtual(true); - Runtime.startConfig("default"); + Runtime.startConfig("dev"); boolean done = true; if (done) { From 5c3b276cd21148d3952cb8f3bde4386997051235 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 19:21:25 -0800 Subject: [PATCH 087/232] a little yolo filter cleaning --- .../myrobotlab/opencv/OpenCVFilterYolo.java | 121 +++++++++++++----- .../org/myrobotlab/service/ServoMixer.java | 16 +-- 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java index 4e409326b7..ce83566839 100755 --- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java +++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java @@ -26,33 +26,44 @@ import org.bytedeco.opencv.opencv_dnn.Net; import org.myrobotlab.document.Classification; import org.myrobotlab.framework.Service; -import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.math.geometry.Rectangle; -import org.myrobotlab.service.OpenCV; import org.slf4j.Logger; -/** - * This filter uses the Yolo image recognition libraries. - * For more information about yolo, here's a link: - * https://pjreddie.com/darknet/yolo/ - * - */ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { private static final long serialVersionUID = 1L; public final static Logger log = LoggerFactory.getLogger(OpenCVFilterYolo.class); - volatile protected Boolean running; - // offset to where the confidence level is in the output matrix of the darknet. + + protected Boolean running; + + // zero offset to where the confidence level is in the output matrix of the + // darknet. private static final int CONFIDENCE_INDEX = 4; + transient private final OpenCVFrameConverter.ToIplImage grabberConverter = new OpenCVFrameConverter.ToIplImage(); + private float confidenceThreshold = 0.25F; - public String darknetHome = FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); + // the column in the detection matrix that contains the confidence level. (I + // think?) + // int probability_index = 5; + // yolo file locations + // private String darknetHome = "c:/dev/workspace/darknet/"; + + // *** the 'correct' way *** + // public String darknetHome = + // FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); + public String darknetHome = "resource/OpenCV/yolo"; // FileIO.gluePaths(Service.getResourceDir(OpenCV.class),"yolo"); public String modelConfig = "yolov2.cfg"; public String modelWeights = "yolov2.weights"; public String modelNames = "coco.names"; + + int classifierThreadCount = 0; + transient DecimalFormat df2 = new DecimalFormat("#.###"); + transient private OpenCVFrameConverter.ToIplImage converterToIpl = new OpenCVFrameConverter.ToIplImage(); + boolean debug = false; transient private Net net; ArrayList classNames; @@ -60,7 +71,6 @@ public class OpenCVFilterYolo extends OpenCVFilter implements Runnable { transient private volatile IplImage lastImage = null; private volatile boolean pending = false; transient private Thread classifier; - volatile Object lock = new Object(); public OpenCVFilterYolo(String name) { super(name); @@ -72,9 +82,11 @@ public OpenCVFilterYolo() { } private void loadYolo() { - log.info("Loading Yolo model."); + log.info("loadYolo - begin"); + try { net = readNetFromDarknet(darknetHome + File.separator + modelConfig, darknetHome + File.separator + modelWeights); + log.info("Loaded yolo darknet model to opencv"); } catch (Exception e) { log.error("readNetFromDarknet could not read", e); return; @@ -86,10 +98,12 @@ private void loadYolo() { log.warn("Error unable to load class names from file {}", modelNames, e); return; } - log.info("Yolo model loaded."); + log.info("Done loading model.."); + log.info("loadYolo - end"); } private ArrayList loadClassNames(String filename) throws IOException { + log.info("loadClassNames - begin"); ArrayList names = new ArrayList(); FileReader fileReader = new FileReader(filename); BufferedReader bufferedReader = new BufferedReader(fileReader); @@ -99,16 +113,18 @@ private ArrayList loadClassNames(String filename) throws IOException { names.add(line.trim()); i++; } - log.info("read {} class names", i); + log.info("read {} names", i); fileReader.close(); + + log.info("loadClassNames - end"); return names; } @Override public IplImage process(IplImage image) throws InterruptedException { - // TODO: what is this doing here? if (lastResult != null) { - // the thread running will be updating lastResult for it as fast as itcan. + // the thread running will be updating lastResult for it as fast as it + // can. // displayResult(image, lastResult); } // ok now we just need to update the image that the current thread is @@ -137,25 +153,29 @@ public void run() { running = true; // loading the model takes a lot of time, we want to block enable/disable // until we are actually running - then we notifyAll + while (running && enabled) { if (!pending) { - // avoid spinning the cpu too hard. + log.debug("Skipping frame"); Thread.sleep(10); continue; } - log.info("Yolo Image Frame - Begin"); + + log.info("process - begin"); // only classify this if we haven't already classified it. if (lastImage != null) { + // lastResult = dl4j.classifyImageVGG16(lastImage); + log.debug("Doing yolo..."); lastResult = yoloFrame(lastImage); - // update this so we will process the next frame that arrives. + // log.info("Yolo done."); + // we processed, next object we'll pick up. pending = false; count++; if (count % 10 == 0) { double rate = 1000.0 * count / (System.currentTimeMillis() - start); log.info("Yolo Classification Rate : {}", rate); } - // TODO: why don't we just publish the lastResult object instead? - // This seems silly.. and potentially this looses data? + Map> ret = new TreeMap<>(); for (Classification c : lastResult) { List nl = null; @@ -167,12 +187,17 @@ public void run() { } nl.add(c); } + invoke("publishClassification", ret); } else { - // We shouldn't see this? - log.info("Waiting for a frame to process."); + log.info("No Image to classify..."); } - } + // TODO: see why there's a race condition. i seem to need a little delay + // here o/w the recognition never seems to start. + // maybe lastImage needs to be marked as volatile ? + + Thread.sleep(1); + } // while (running) } catch (Exception e) { log.error("yolo thread threw", e); @@ -181,23 +206,38 @@ public void run() { synchronized (lock) { classifier = null; } - + log.info("yolo exiting classifier thread"); + log.info("run - end"); } private ArrayList yoloFrame(IplImage frame) { + log.debug("Starting yolo on frame..."); + log.info("yoloFrame - begin"); // this is our list of objects that have been detected in a given frame. ArrayList yoloObjects = new ArrayList(); // convert that frame to a matrix (Mat) using the frame converters in javacv + + log.info("yoloFrame - grabberConverter {}", frame); + // log.info("Yolo frame start"); Mat inputMat = grabberConverter.convertToMat(grabberConverter.convert(frame)); + // log.info("Input mat created"); // TODO: I think yolo expects RGB color (which is inverted in the next step) // so if the input image isn't in RGB color, we might need a cvCutColor + log.info("yoloFrame - blobFromImage"); Mat inputBlob = blobFromImage(inputMat, 1 / 255.F, new Size(416, 416), new Scalar(), true, false, CV_32F); // put our frame/input blob into the model. + // log.info("input blob created"); + log.info("yoloFrame - blob {}", inputBlob); net.setInput(inputBlob); + + log.debug("Feed forward!"); + // log.info("Input blob set on network."); // ask for the detection_out layer i guess? not sure the details of the // forward method, but this computes everything like magic! Mat detectionMat = net.forward("detection_out"); + // log.info("output detection matrix produced"); + log.debug("detection matrix computed"); // iterate the rows of the detection matrix. for (int i = 0; i < detectionMat.rows(); i++) { Mat currentRow = detectionMat.row(i); @@ -206,11 +246,13 @@ private ArrayList yoloFrame(IplImage frame) { // skip the noise continue; } + // System.out.println("\nCurrent row has " + currentRow.size().width() + // "=width " + currentRow.size().height() + "=height."); // currentRow.position(probability_index); // int probability_size = detectionMat.cols() - probability_index; // detectionMat; + // String className = getWithDefault(classNames, i); // System.out.print("\nROW (" + className + "): " + // currentRow.getFloatBuffer().get(4) + " -- \t\t"); @@ -231,57 +273,69 @@ private ArrayList yoloFrame(IplImage frame) { // ok. in theory this is something we think it might actually be. float x = currentRow.getFloatBuffer().get(0); float y = currentRow.getFloatBuffer().get(1); + float width = currentRow.getFloatBuffer().get(2); float height = currentRow.getFloatBuffer().get(3); int xLeftBottom = (int) ((x - width / 2) * inputMat.cols()); int yLeftBottom = (int) ((y - height / 2) * inputMat.rows()); int xRightTop = (int) ((x + width / 2) * inputMat.cols()); int yRightTop = (int) ((y + height / 2) * inputMat.rows()); + if (xLeftBottom < 0) { xLeftBottom = 0; } if (yLeftBottom < 0) { yLeftBottom = 0; } + // crop the right top if (xRightTop > inputMat.cols()) { xRightTop = inputMat.cols(); } + if (yRightTop > inputMat.rows()) { yRightTop = inputMat.rows(); } + log.debug(label + " (" + confidence + "%) [(" + xLeftBottom + "," + yLeftBottom + "),(" + xRightTop + "," + yRightTop + ")]"); Rect boundingBox = new Rect(xLeftBottom, yLeftBottom, xRightTop - xLeftBottom, yRightTop - yLeftBottom); // grab just the bytes for the ROI defined by that rect.. // get that as a mat, save it as a byte array (png?) other encoding? // TODO: have a target size? + + IplImage cropped = extractSubImage(inputMat, boundingBox); if (debug) { debug = false; - IplImage cropped = extractSubImage(inputMat, boundingBox); show(cropped, "detected img"); } Classification obj = new Classification(String.format("%s.%s-%d", data.getName(), name, data.getFrameIndex())); obj.setLabel(label); obj.setBoundingBox(xLeftBottom, yLeftBottom, xRightTop - xLeftBottom, yRightTop - yLeftBottom); obj.setConfidence(confidence); - // TODO: add the original frame converted as a serializable image ( BufferedImage or png byte array? ) // obj.setImage(data.getDisplay()); - // we might just want to provide a reference to the frame. such as the frame number or something similar so if - // we want to find the original frame we can look it up. (in solr?) + // for non-serializable "local" image objects + obj.setObject(frame); yoloObjects.add(obj); } } } + log.info("yoloFrame - end"); return yoloObjects; } private IplImage extractSubImage(Mat inputMat, Rect boundingBox) { + log.info("extractSubImage - begin"); + // log.debug(boundingBox.x() + " " + boundingBox.y() + " " + boundingBox.width() + " " + boundingBox.height()); + // TODO: figure out if the width/height is too large! don't want to go array // out of bounds Mat cropped = new Mat(inputMat, boundingBox); + IplImage image = converterToIpl.convertToIplImage(converterToIpl.convert(cropped)); // This mat should be the cropped image! + + log.info("extractSubImage - end"); return image; } @@ -290,8 +344,10 @@ public void release() { // synchronized (lock) { log.info("release - begin"); disable(); // blocks until ready + // while(isRunning){ sleep(30) .. check again } // bleed out the thread before deallocating + if (net != null) { net.deallocate(); net = null; @@ -300,6 +356,8 @@ public void release() { // } } + volatile Object lock = new Object(); + @Override public void enable() { if (classifier != null) { @@ -336,13 +394,16 @@ public void disable() { @Override public BufferedImage processDisplay(Graphics2D graphics, BufferedImage image) { if (lastResult != null) { + for (Classification obj : lastResult) { String label = obj.getLabel() + " (" + df2.format(obj.getConfidence() * 100) + "%)"; + Rectangle bb = obj.getBoundingBox(); int x = (int) bb.x; int y = (int) bb.y; int width = (int) bb.width; int height = (int) bb.height; + graphics.setColor(Color.BLACK); graphics.drawRect(x, y, width, height); graphics.fillRect(x, y - 20, 7 * label.length(), 20); diff --git a/src/main/java/org/myrobotlab/service/ServoMixer.java b/src/main/java/org/myrobotlab/service/ServoMixer.java index 03395f6cc6..dc6c3f7127 100755 --- a/src/main/java/org/myrobotlab/service/ServoMixer.java +++ b/src/main/java/org/myrobotlab/service/ServoMixer.java @@ -676,24 +676,14 @@ public void stopService() { public static void main(String[] args) throws Exception { try { - LoggingFactory.init("WARN"); - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - - - boolean done = true; - if (done) { - return; - } - - Runtime.main(new String[] { "--id", "admin"}); LoggingFactory.init("INFO"); Runtime.start("i01.head.rothead", "Servo"); Runtime.start("i01.head.neck", "Servo"); + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); Python python = (Python) Runtime.start("python", "Python"); ServoMixer mixer = (ServoMixer) Runtime.start("mixer", "ServoMixer"); } catch (Exception e) { From 2953d7229589a1e1c18716bf5e71999d8ac97066 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 15 Nov 2023 19:39:43 -0800 Subject: [PATCH 088/232] synching with develop --- src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java index 5c8da6bdea..8851544ab1 100644 --- a/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java +++ b/src/main/java/org/myrobotlab/framework/repo/IvyWrapper.java @@ -535,7 +535,7 @@ synchronized public void install(String location, String[] serviceTypes) throws } } - publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of %s", (Object[]) serviceTypes))); + publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of artifacts for %s", (Object[]) serviceTypes))); } publishStatus(Status.newInstance(Repo.class.getSimpleName(), StatusLevel.INFO, Repo.INSTALL_FINISHED, String.format("finished install of %s", (Object[]) serviceTypes))); From be14db57406979feff4704809e8e6d20f0dd3755 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 23 Nov 2023 07:21:09 -0800 Subject: [PATCH 089/232] Neopixel with single thread controller queu --- .../java/org/myrobotlab/codec/CodecUtils.java | 70 +- .../org/myrobotlab/service/AudioFile.java | 16 +- .../java/org/myrobotlab/service/NeoPixel.java | 985 +++++++++--------- .../service/config/NeoPixelConfig.java | 122 ++- .../service/data/LedDisplayData.java | 77 +- .../service/interfaces/AudioListener.java | 3 +- .../service/interfaces/AudioPublisher.java | 3 +- .../resources/resource/NeoPixel/NeoPixel.py | 142 +-- .../WebGui/app/service/js/NeoPixelGui.js | 4 +- .../WebGui/app/service/views/NeoPixelGui.html | 2 +- 10 files changed, 855 insertions(+), 569 deletions(-) diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java index bd512189b8..ba528b7a10 100644 --- a/src/main/java/org/myrobotlab/codec/CodecUtils.java +++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java @@ -1,5 +1,6 @@ package org.myrobotlab.codec; +import java.awt.Color; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; @@ -188,7 +189,7 @@ public class CodecUtils { private static final ObjectMapper mapper = new ObjectMapper(); /** - * The pretty printer to be used with {@link #mapper} + * The pretty printer to be used with {@link #mapper} */ private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter(); @@ -964,9 +965,8 @@ public static String getSafeReferenceName(String name) { } /** - * Serializes the specified object to JSON, using - * {@link #mapper} with {@link #jacksonPrettyPrinter} to pretty-ify the - * result. + * Serializes the specified object to JSON, using {@link #mapper} with + * {@link #jacksonPrettyPrinter} to pretty-ify the result. * * @param ret * The object to be serialized @@ -1095,12 +1095,13 @@ static public Message pathToMsg(String from, String path) { // path parts less than 3 is a dir or ls if (parts.length < 3) { // this morphs a path which has less than 3 parts - // into a runtime "ls" method call to do reflection of services or service methods + // into a runtime "ls" method call to do reflection of services or + // service methods // e.g. /clock -> /runtime/ls/"/clock" // e.g. /clock/ -> /runtime/ls/"/clock/" msg.method = "ls"; - msg.data = new Object[] { "\"" + path + "\""}; + msg.data = new Object[] { "\"" + path + "\"" }; return msg; } @@ -1483,7 +1484,8 @@ public static boolean isLocal(String name, String id) { } public static ServiceConfig readServiceConfig(String filename) throws IOException { - return readServiceConfig(filename, new StaticType<>() {}); + return readServiceConfig(filename, new StaticType<>() { + }); } /** @@ -1629,4 +1631,58 @@ public static byte[] fromBase64(String input) { return Base64.getDecoder().decode(input); } + public static int[] getColor(String value) { + String hex = getColorHex(value); + if (hex != null) { + return hexToRGB(hex); + } + return hexToRGB(value); + } + + public static List getColorNames() { + Field[] colorFields = Color.class.getDeclaredFields(); + List colorNames = new ArrayList<>(); + + for (Field field : colorFields) { + if (field.getType().equals(Color.class)) { + colorNames.add(field.getName()); + } + } + return colorNames; + } + + public static String getColorHex(String colorName) { + Color color; + try { + color = (Color) Color.class.getField(colorName.toLowerCase()).get(null); + } catch (Exception e) { + return null; + } + return String.format("#%06X", (0xFFFFFF & color.getRGB())); + } + + public static int[] hexToRGB(String hexValue) { + if (hexValue == null) { + return null; + } + int[] rgb = new int[3]; + try { + // Check if the hex value starts with '#' and remove it if present + if (hexValue.startsWith("#")) { + hexValue = hexValue.substring(1); + } + + if (hexValue.startsWith("0x")) { + hexValue = hexValue.substring(2); + } + + // Parse the hex string into integers for red, green, and blue components + rgb[0] = Integer.parseInt(hexValue.substring(0, 2), 16); // Red + rgb[1] = Integer.parseInt(hexValue.substring(2, 4), 16); // Green + rgb[2] = Integer.parseInt(hexValue.substring(4, 6), 16); // Blue + } catch (NumberFormatException | StringIndexOutOfBoundsException e) { + log.error("Invalid hex color value {}", hexValue); + } + return rgb; + } } diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java index 104db4dce0..3997f6960e 100644 --- a/src/main/java/org/myrobotlab/service/AudioFile.java +++ b/src/main/java/org/myrobotlab/service/AudioFile.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,17 +45,17 @@ import org.myrobotlab.audio.AudioProcessor; import org.myrobotlab.audio.PlaylistPlayer; import org.myrobotlab.framework.Service; +import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.net.Http; import org.myrobotlab.service.config.AudioFileConfig; -import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.interfaces.AudioControl; +import org.myrobotlab.service.interfaces.AudioListener; import org.myrobotlab.service.interfaces.AudioPublisher; import org.slf4j.Logger; -import java.util.Random; /** * * AudioFile - This service can be used to play an audio file such as an mp3. @@ -128,7 +129,16 @@ public class AudioFile extends Service implements AudioPublishe final private transient PlaylistPlayer playlistPlayer = new PlaylistPlayer(this); - + public void attach(Attachable attachable) { + if (attachable instanceof AudioListener) { + attachAudioListener(attachable.getName()); + } + } + + public void attach(AudioListener listener) { + attachAudioListener(listener.getName()); + } + public void setPeakMultiplier(double peakMultiplier) { AudioFileConfig c = (AudioFileConfig)config; c.peakMultiplier = peakMultiplier; diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index e13daf273d..ee4f72177e 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -28,9 +28,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.framework.interfaces.ServiceInterface; @@ -38,58 +40,15 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.service.config.NeoPixelConfig; -import org.myrobotlab.service.config.ServiceConfig; +import org.myrobotlab.service.config.NeoPixelConfig.Flash; +import org.myrobotlab.service.data.AudioData; import org.myrobotlab.service.data.LedDisplayData; +import org.myrobotlab.service.interfaces.AudioListener; import org.myrobotlab.service.interfaces.NeoPixelControl; import org.myrobotlab.service.interfaces.NeoPixelController; import org.slf4j.Logger; -public class NeoPixel extends Service implements NeoPixelControl { - - /** - * Thread to do animations Java side and push the changing of pixels to the - * neopixel - */ - private class AnimationRunner implements Runnable { - - boolean running = false; - - private transient Thread thread = null; - - @Override - public void run() { - try { - running = true; - - while (running) { - equalizer(); - Double wait_ms_per_frame = fpsToWaitMs(speedFps); - sleep(wait_ms_per_frame.intValue()); - } - } catch (Exception e) { - error(e); - stop(); - } - } - - // FIXME - this should just wait/notify - not start a thread - public synchronized void start() { - running = false; - thread = new Thread(this, String.format("%s-animation-runner", getName())); - thread.start(); - } - - public synchronized void stop() { - running = false; - thread = null; - } - } - - @Override - public void releaseService() { - super.releaseService(); - clear(); - } +public class NeoPixel extends Service implements NeoPixelControl, AudioListener { public static class Pixel { public int address; @@ -122,6 +81,7 @@ public String toString() { return String.format("%d:%d,%d,%d,%d", address, red, green, blue, white); } } + public static class PixelSet { public long delayMs = 0; @@ -138,7 +98,7 @@ public int[] flatten() { ret[j + 1] = p.red; ret[j + 2] = p.green; ret[j + 3] = p.blue; - // lame .. using the same strategy as original neopix + // lame .. using the same strategy as original neopixel // bucket of 4 bytes... ret[j + 4] = p.white; } @@ -146,19 +106,167 @@ public int[] flatten() { } } - public final static Logger log = LoggerFactory.getLogger(NeoPixel.class); - - private static final long serialVersionUID = 1L; - /** - * thread for doing off board and in memory animations + * Thread to do animations Java side and push the changing of pixels to the + * neopixel */ - protected final AnimationRunner animationRunner; + private class Worker implements Runnable { + + boolean running = false; + + private transient Thread thread = null; + + @Override + public void run() { + running = true; + while (running) { + try { + LedDisplayData display = displayQueue.take(); + // get led display data + log.error(display.toString()); + + NeoPixelController npc = (NeoPixelController) Runtime.getService(controller); + if (npc == null) { + error("%s cannot process display data controller not set", getName()); + continue; + } + + if ("animation".equals(display.action)) { + sleep(100); + Double fps = fpsToWaitMs(speedFps); + npc.neoPixelSetAnimation(getName(), animations.get(display.animation), red, green, blue, white, fps.intValue()); + currentAnimation = display.animation; + } else if ("clear".equals(display.action)) { + sleep(100); + npc.neoPixelClear(getName()); + currentAnimation = null; + } else if ("writeMatrix".equals(display.action)) { + sleep(100); + npc.neoPixelWriteMatrix(getName(), getPixelSet().flatten()); + } else if ("fill".equals(display.action)) { + Flash f = display.flashes.get(0); + sleep(100); + npc.neoPixelFill(getName(), display.beginAddress, display.onCount, f.red, f.green, f.blue, f.white); + } else if ("brightness".equals(display.action)) { + sleep(100); + display.brightness = (display.brightness > 255)?255:display.brightness; + display.brightness = (display.brightness < 0)?0:display.brightness; + npc.neoPixelSetBrightness(getName(), display.brightness); + } else if ("flash".equals(display.action)) { + + // FIXME disable currentAnimation ??? // save it ? + sleep(100); + npc.neoPixelClear(getName()); + for (int count = 0; count < display.flashes.size(); count++) { + Flash flash = display.flashes.get(count); + npc.neoPixelFill(getName(), 0, count, flash.red, flash.green, flash.blue, flash.white); + sleep(flash.timeOn); + npc.neoPixelClear(getName()); + sleep(flash.timeOff); + } + } + // start animations + // playAnimation(lastAnimation); + } catch (InterruptedException ex) { + log.info("shutting down worker"); + } catch (Exception e) { + error(e); + } + } + running = false; + } + + // FIXME - this should just wait/notify - not start a thread + public synchronized void start() { + running = false; + thread = new Thread(this, String.format("%s-animation-runner", getName())); + thread.start(); + } + + public synchronized void stop() { + running = false; + if (thread != null) { + thread.interrupt(); + } + thread = null; + } + } + + public final static Logger log = LoggerFactory.getLogger(NeoPixel.class); /** - * current selected red value + * maximum actions on the display queue */ - protected int red = 0; + private final static int MAX_QUEUE = 200; + + private static final long serialVersionUID = 1L; + + + + public static void main(String[] args) throws InterruptedException { + + try { + + LoggingFactory.init(Level.WARN); + + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); + webgui.autoStartBrowser(false); + webgui.startService(); + + boolean done = true; + if (done) { + return; + } + + Runtime.start("python", "Python"); + Polly polly = (Polly) Runtime.start("polly", "Polly"); + + Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino"); + arduino.connect("/dev/ttyACM0"); + + NeoPixel neopixel = (NeoPixel) Runtime.start("neopixel", "NeoPixel"); + + neopixel.setPin(26); + neopixel.setPixelCount(8); + // neopixel.attach(arduino, 5, 8, 3); + neopixel.attach(arduino); + neopixel.clear(); + neopixel.fill(0, 8, 0, 0, 120); + neopixel.setPixel(2, 120, 0, 0); + neopixel.setPixel(3, 0, 120, 0); + neopixel.setBrightness(20); + neopixel.setBrightness(40); + neopixel.setBrightness(80); + neopixel.setBrightness(160); + neopixel.setBrightness(200); + neopixel.setBrightness(10); + neopixel.setBrightness(255); + neopixel.setAnimation(5, 80, 80, 0, 40); + + neopixel.attach(polly); + + neopixel.clear(); + // neopixel.detach(arduino); + // arduino.detach(neopixel); + + polly.speak("i'm sorry dave i can't let you do that"); + polly.speak(" I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do"); + polly.speak("I've just picked up a fault in the AE35 unit. It's going to go 100% failure in 72 hours."); + polly.speak("This mission is too important for me to allow you to jeopardize it."); + polly.speak("I've got a bad feeling about it."); + polly.speak("I'm sorry, Dave. I'm afraid I can't do that."); + polly.speak("Look Dave, I can see you're really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over."); + + // neopixel.test(); + // neopixel.detach(arduino); + // neopixel.detach(polly); + + } catch (Exception e) { + log.error("main threw", e); + } + } + + protected final Map animations = new HashMap<>(); /** * current selected blue value @@ -166,19 +274,19 @@ public int[] flatten() { protected int blue = 0; /** - * current selected green value + * 0 = off / 255 brightest */ - protected int green = 120; + protected int brightness = 255; /** - * white if available + * name of controller currently attached to */ - protected int white = 0; + protected String controller = null; /** - * name of controller currently attached to + * currently selected animation */ - protected String controller = null; + protected String currentAnimation; /** * name of current matrix @@ -190,12 +298,21 @@ public int[] flatten() { */ protected int currentSequence = 0; + private BlockingQueue displayQueue = new ArrayBlockingQueue<>(MAX_QUEUE); + + /** + * current selected green value + */ + protected int green = 120; + /** * A named set of sequences of pixels initially you start with "default" but * if you can choose to name and save sequences */ Map> matrices = new HashMap<>(); + private int maxFps = 50; + /** * pin NeoPixel is attached to on controller */ @@ -212,34 +329,31 @@ public int[] flatten() { protected int pixelDepth = 3; /** - * currently selected animation + * current selected red value */ - protected String currentAnimation; + protected int red = 0; /** * speed of an animation in fps */ protected int speedFps = 10; - private int maxFps = 50; - protected String type = "RGB"; /** - * 0 = off / 255 brightest + * white if available */ - protected int brightness = 255; - - final Map animations = new HashMap<>(); + protected int white = 0; - public Set getAnimations() { - return animations.keySet(); - } + /** + * thread for doing off board and in memory animations + */ + protected final Worker worker; public NeoPixel(String n, String id) { super(n, id); registerForInterfaceChange(NeoPixelController.class); - animationRunner = new AnimationRunner(); + worker = new Worker(); animations.put("Stop", 1); animations.put("Color Wipe", 2); animations.put("Larson Scanner", 3); @@ -249,8 +363,58 @@ public NeoPixel(String n, String id) { animations.put("Rainbow Cycle", 7); animations.put("Flash Random", 8); animations.put("Ironman", 9); - // > 99 is java side animations - animations.put("Equalizer", 100); + } + + private void addDisplayTask(LedDisplayData data) { + if (displayQueue.size() > MAX_QUEUE - 1) { + warn("dropping display task"); + } else { + displayQueue.add(data); + } + } + + @Deprecated /* use clear() */ + public void animationStop() { + clear(); + } + + @Override + public NeoPixelConfig apply(NeoPixelConfig c) { + super.apply(c); + // FIXME - remove local fields in favor of config + setPixelDepth(config.pixelDepth); + + if (config.pixelCount != null) { + setPixelCount(config.pixelCount); + } + + setSpeed(config.speed); + if (config.pin != null) { + setPin(config.pin); + } + red = config.red; + green = config.green; + blue = config.blue; + if (config.controller != null) { + try { + attach(config.controller); + } catch (Exception e) { + error(e); + } + } + + if (config.currentAnimation != null) { + playAnimation(config.currentAnimation); + } + + if (config.brightness != null) { + setBrightness(config.brightness); + } + + if (config.fill) { + fillMatrix(red, green, blue); + } + return c; } @Override @@ -289,40 +453,15 @@ public void attachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - @Deprecated /* use clear() */ - public void animationStop() { - clear(); - } - - @Override - public boolean isAttached(Attachable instance) { - return instance.getName().equals(controller); - } - @Override public void clear() { if (controller == null) { error("%s cannot clear - not attached to controller", getName()); return; } - - // stop java animations - animationRunner.stop(); - // stop on board controller animations - setAnimation(0, 0, 0, 0, speedFps); - clearPixelSet(); log.debug("clear getPixelSet {}", getPixelSet().flatten()); - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot writeMatrix controller not set", getName()); - return; - } - - currentAnimation = null; - - np2.neoPixelClear(getName()); + addDisplayTask(new LedDisplayData("clear")); } public void clearPixelSet() { @@ -361,154 +500,165 @@ public void detachNeoPixelController(NeoPixelController neoCntrlr) { broadcastState(); } - public void equalizer() { - equalizer(null, null); + public void fill(int r, int g, int b) { + fill(0, pixelCount, r, g, b, null); } - public void equalizer(Long wait_ms_per_frame, Integer range) { + public void fill(int beginAddress, int count, int r, int g, int b) { + fill(beginAddress, count, r, g, b, null); + } - if (controller == null) { - log.warn("controller not set"); - return; + public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { + if (w == null) { + w = 0; } + LedDisplayData data = new LedDisplayData("fill"); + data.beginAddress = 0; + data.onCount = count; + data.flashes.add(new Flash(r, g, b, 500, 500)); + addDisplayTask(data); + } - if (wait_ms_per_frame == null) { - wait_ms_per_frame = 25L; + public void fill(String color) { + int rgb[] = CodecUtils.getColor(color); + if (rgb == null) { + error("could not get color %s", color); + return; } + fill(rgb[0], rgb[1], rgb[2]); + } - if (range == null) { - range = 25; - } - - Random rand = new Random(); - int c = rand.nextInt(range); - - fillMatrix(red, green, blue, white); + public void fillMatrix(int r, int g, int b) { + fillMatrix(r, g, b, 0); + } - if (c < 18) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); + public void fillMatrix(int r, int g, int b, int w) { + PixelSet ps = getPixelSet(); + for (Pixel p : ps.pixels) { + p.red = r; + p.green = g; + p.blue = b; + p.white = w; } + } - fillMatrix(red, green, blue, white); + public void flash() { + flash(red, green, blue, 1, 300, 300); + } - if (c < 16) { - setMatrix(0, 0, 0, 0); - setMatrix(7, 0, 0, 0); - } + public void flash(int r, int g, int b) { + flash(r, g, b, 1, 300, 300); + } - if (c < 12) { - setMatrix(1, 0, 0, 0); - setMatrix(6, 0, 0, 0); - } + public void flash(int r, int g, int b, int count) { + flash(r, g, b, count, 300, 300); + } - if (c < 8) { - setMatrix(2, 0, 0, 0); - setMatrix(5, 0, 0, 0); + public void flash(int r, int g, int b, int count, long timeOn, long timeOff) { + LedDisplayData data = new LedDisplayData("flash"); + data.action = "flash"; + for (int i = 0; i < count; ++i) { + data.flashes.add(new Flash(r, g, b, timeOn, timeOff)); } + addDisplayTask(data); + } - writeMatrix(); - + public void flash(int r, int g, int b, long timeOn, long timeOff) { + flash(r, g, b, 1, timeOn, timeOff); } - - public void onLedDisplay(LedDisplayData data) { - - if ("flash".equals(data.action)) { - flash(data.count, data.interval, data.red, data.green, data.blue); + + /** + * Invokes a flash from the flashMap + * + * @param name + */ + public void flash(String name) { + if (config.flashMap == null) { + error("flash map is null"); + return; } - - } - public void flash(int count, long interval, int r, int g, int b) { - long delay = 0; - for (int i = 0; i < count; ++i) { - addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", r, g, b); - delay+= interval/2; - addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); - delay+= interval/2; + if (config.flashMap.containsKey(name)) { + LedDisplayData display = new LedDisplayData("flash"); + Flash[] flashes = config.flashMap.get(name); + for (int i = 0; i < flashes.length; ++i) { + display.flashes.add(flashes[i]); + } + addDisplayTask(display); + + } else { + error("requested flash %s not found in flash map", name); } } - - public void flashBrightness(double brightNess) { - NeoPixelConfig c = (NeoPixelConfig)config; - - // FIXME - these need to be moved into config -// int count = 2; -// int interval = 75; - setBrightness((int)brightNess); - fill(red, green, blue); - -// long delay = 0; -// for (int i = 0; i < count; ++i) { -// addTask(getName()+"fill-"+System.currentTimeMillis(), true, 0, delay, "fill", red, green, blue); -// delay+= interval/2; -// addTask(getName()+"clear-"+System.currentTimeMillis(), true, 0, delay, "clear"); -// delay+= interval/2; -// } - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - + public void flashBrightness(double brightness) { + LedDisplayData data = new LedDisplayData("brightness"); + // adafruit neopixel library does not recover from setting + // brightness to 0 - so we have to hack around it + if (data.brightness < 10) { + return; + } + addDisplayTask(data); } - public void fill(int r, int g, int b) { - fill(0, pixelCount, r, g, b, null); + // utility to convert frames per second to milliseconds per frame. + private double fpsToWaitMs(int fps) { + if (fps == 0) { + // fps can't be zero. + error("fps can't be zero for neopixel animation defaulting to 1 fps"); + return 1000.0; + } + double result = 1000.0 / fps; + return result; } - public void fill(int beginAddress, int count, int r, int g, int b) { - fill(beginAddress, count, r, g, b, null); + public Set getAnimations() { + return animations.keySet(); } - public void fill(int beginAddress, int count, int r, int g, int b, Integer w) { - NeoPixelConfig c = (NeoPixelConfig)config; - - if (w == null) { - w = 0; - } - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } - np2.neoPixelFill(getName(), beginAddress, count, r, g, b, w); - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - + public int getBlue() { + return blue; } - public void fillMatrix(int r, int g, int b) { - fillMatrix(r, g, b, 0); + /** + * get the list of hex defined colors + * + * @return + */ + public List getColorNames() { + return CodecUtils.getColorNames(); } - public void fillMatrix(int r, int g, int b, int w) { - PixelSet ps = getPixelSet(); - for (Pixel p : ps.pixels) { - p.red = r; - p.green = g; - p.blue = b; - p.white = w; - } - - } + @Override + public NeoPixelConfig getConfig() { + super.getConfig(); + // FIXME - remove local fields in favor of config + config.pin = pin; + config.pixelCount = pixelCount; + config.pixelDepth = pixelDepth; + config.speed = speedFps; + config.red = red; + config.green = green; + config.blue = blue; + config.controller = controller; + config.currentAnimation = currentAnimation; + config.brightness = brightness; - public int getBlue() { - return blue; + return config; } public int getCount() { return pixelCount; } + public Set getFlashNames() { + if (config.flashMap == null) { + return null; + } + return config.flashMap.keySet(); + } + public int getGreen() { return green; } @@ -579,59 +729,102 @@ public int getRed() { } @Override - public void playAnimation(String animation) { + public boolean isAttached(Attachable instance) { + return instance.getName().equals(controller); + } - if (animations.containsKey(animation)) { - currentAnimation = animation; - if (animations.get(animation) < 99) { - setAnimation(animations.get(animation), red, green, blue, speedFps); - } else { - // only 1 java side animation at the moment - equalizer(); - animationRunner.start(); - } - } else { - error("could not find animation %s", animation); + /** + * Publishes a flash based on a predefined name + * + * @param name + */ + public void onFlash(String name) { + flash(name); + } + + public void onLedDisplay(LedDisplayData data) { + try { + addDisplayTask(data); + } catch (IllegalStateException e) { + log.info("queue full"); } - broadcastState(); } - public void stopAnimation() { - setAnimation(1, red, green, blue, speedFps); + /** + * takes a scalar value and fills with the appropriate brightness + * using the peak color if available + * @param value + */ + public void onPeak(double value) { + flashBrightness(value); + } + + public void onPlayAnimation(String animation) { + playAnimation(animation); + } + + public String onStarted(String name) { + return name; + } + + public void onStopAnimation() { + stopAnimation(); } @Override - public void setAnimation(int animation, int red, int green, int blue, int speedFps) { - if (speedFps > maxFps) { - speedFps = maxFps; - } + synchronized public void playAnimation(String animation) { - this.speedFps = speedFps; + log.debug("playAnimation {} {} {} {} {}", animation, red, green, blue, speedFps); - if (controller == null) { - error("%s could not set animation no attached controller", getName()); + if (animation == null || animation.equals("Stop")) { + log.info("clearing animation"); + clear(); return; } - log.debug("setAnimation {} {} {} {} {}", animation, red, green, blue, speedFps); - NeoPixelController nc2 = (NeoPixelController) Runtime.getService(controller); - Double wait_ms_per_frame = fpsToWaitMs(speedFps); - nc2.neoPixelSetAnimation(getName(), animation, red, green, blue, 0, wait_ms_per_frame.intValue()); - if (animation == 1) { - currentAnimation = null; - animationRunner.stop(); + + if (animation.equals(currentAnimation)) { + log.info("already playing {}", currentAnimation); + return; + } + + if (animations.containsKey(animation)) { + + if (speedFps > maxFps) { + speedFps = maxFps; + } + + LedDisplayData data = new LedDisplayData("animation"); + data.animation = animation; + addDisplayTask(data); + } else { + error("could not find animation %s", animation); } - broadcastState(); } - // utility to convert frames per second to milliseconds per frame. - private double fpsToWaitMs(int fps) { - if (fps == 0) { - // fps can't be zero. - error("fps can't be zero for neopixel animation defaulting to 1 fps"); - return 1000.0; + public void playIronman() { + setColor(170, 170, 255); + setSpeed(50); + playAnimation("Ironman"); + } + + @Override + public void releaseService() { + super.releaseService(); + clear(); + } + + @Override + @Deprecated /* use playAnimation */ + public void setAnimation(int animation, int red, int green, int blue, int speedFps) { + setRed(red); + setGreen(green); + setBlue(blue); + setSpeed(speedFps); + for (String animationName : animations.keySet()) { + if (animations.get(animationName) == animation) { + playAnimation(animationName); + } } - double result = 1000.0 / fps; - return result; } @Override @@ -653,22 +846,32 @@ public void setBlue(int blue) { } public void setBrightness(int value) { - NeoPixelConfig c = (NeoPixelConfig)config; - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } brightness = value; - np2.neoPixelSetBrightness(getName(), value); - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); + LedDisplayData data = new LedDisplayData("brightness"); + data.brightness = value; + addDisplayTask(data); + } + + public void setColor(int red, int green, int blue) { + this.red = red; + this.green = green; + this.blue = blue; + if (currentAnimation != null) { + // restarting currently running animation + playAnimation(currentAnimation); } + } + /** + * can be hex #FFFFFE 0xFFEEFF FFEEFF or grey, blue, yellow etc + * + * @param color + */ + public void setColor(String color) { + int[] rgb = CodecUtils.getColor(color); + setRed(rgb[0]); + setGreen(rgb[1]); + setBlue(rgb[2]); } public void setGreen(int green) { @@ -693,6 +896,19 @@ public void setPin(int pin) { broadcastState(); } + @Override + public void setPin(String pin) { + try { + if (pin == null) { + this.pin = null; + return; + } + this.pin = Integer.parseInt(pin); + } catch (Exception e) { + error(e); + } + } + /** * basic setting of a pixel */ @@ -718,7 +934,6 @@ public void setPixel(int address, int red, int green, int blue, int white) { * @param delayMs */ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int red, int green, int blue, int white, Integer delayMs) { - NeoPixelConfig c = (NeoPixelConfig)config; // get and update memory cache PixelSet ps = getPixelSet(matrixName, pixelSetIndex); @@ -737,21 +952,8 @@ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int // update memory ps.pixels.set(address, pixel); - - // write immediately - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot setPixel controller not set", getName()); - return; - } - - np2.neoPixelWriteMatrix(getName(), pixel.flatten()); - - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } + LedDisplayData data = new LedDisplayData("writeMatrix"); + addDisplayTask(data); } public int setPixelCount(int pixelCount) { @@ -770,67 +972,10 @@ public void setPixelDepth(int depth) { broadcastState(); } - public void setType(String type) { - if ("RGB".equals(type) || "RGBW".equals(type)) { - this.type = type; - if (type.equals("RGB")) { - pixelDepth = 3; - } else { - pixelDepth = 4; - } - broadcastState(); - } else { - error("type %s invalid only RGB or RGBW", type); - } - } - public void setRed(int red) { this.red = red; } - public void startAnimation() { - startAnimation(currentMatrix); - } - - /** - * handle both user defined, java defined, and controller on board animations - * FIXME - make "settings" separate call - * - * @param name - */ - public void startAnimation(String name) { - animationRunner.start(); - } - - public void setColor(int red, int green, int blue) { - this.red = red; - this.green = green; - this.blue = blue; - if (currentAnimation != null) { - // restarting currently running animation - playAnimation(currentAnimation); - } - } - - @Override - public void writeMatrix() { - NeoPixelConfig c = (NeoPixelConfig)config; - - NeoPixelController np2 = (NeoPixelController) Runtime.getService(controller); - if (controller == null || np2 == null) { - error("%s cannot writeMatrix controller not set", getName()); - return; - } - np2.neoPixelWriteMatrix(getName(), getPixelSet().flatten()); - if (c.autoClear) { - purgeTask("clear"); - // and start our countdown - addTaskOneShot(c.idleTimeout, "clear"); - } - - - } - /** * extremely rough fps * @@ -849,155 +994,51 @@ public void setSpeed(Integer speed) { } } - public void playIronman() { - setColor(170, 170, 255); - setSpeed(50); - playAnimation("Ironman"); + public void setType(String type) { + if ("RGB".equals(type) || "RGBW".equals(type)) { + this.type = type; + if (type.equals("RGB")) { + pixelDepth = 3; + } else { + pixelDepth = 4; + } + broadcastState(); + } else { + error("type %s invalid only RGB or RGBW", type); + } } @Override - public ServiceConfig getConfig() { - - NeoPixelConfig config = (NeoPixelConfig)super.getConfig(); - // FIXME - remove local fields in favor of config - config.pin = pin; - config.pixelCount = pixelCount; - config.pixelDepth = pixelDepth; - config.speed = speedFps; - config.red = red; - config.green = green; - config.blue = blue; - config.controller = controller; - config.currentAnimation = currentAnimation; - config.brightness = brightness; - - return config; + public void startService() { + super.startService(); + worker.start(); } - @Override - public ServiceConfig apply(ServiceConfig c) { - NeoPixelConfig config = (NeoPixelConfig) super.apply(c); - // FIXME - remove local fields in favor of config - setPixelDepth(config.pixelDepth); - - if (config.pixelCount != null) { - setPixelCount(config.pixelCount); - } - - setSpeed(config.speed); - if (config.pin != null) { - setPin(config.pin); - } - red = config.red; - green = config.green; - blue = config.blue; - if (config.controller != null) { - try { - attach(config.controller); - } catch (Exception e) { - error(e); - } - } - - if (config.currentAnimation != null) { - playAnimation(config.currentAnimation); - } - - if (config.brightness != null) { - setBrightness(config.brightness); - } - - if (config.fill) { - fillMatrix(red, green, blue); - } - - return c; + synchronized public void stopAnimation() { + clear(); } - public String onStarted(String name) { - return name; + public void stopService() { + super.stopService(); + worker.stop(); + clear(); } - public static void main(String[] args) throws InterruptedException { - - try { - - LoggingFactory.init(Level.INFO); - - WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); - webgui.autoStartBrowser(false); - webgui.startService(); - - boolean done = true; - if (done) { - return; - } - - Runtime.start("python", "Python"); - Polly polly = (Polly) Runtime.start("polly", "Polly"); - - Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino"); - arduino.connect("/dev/ttyACM0"); - - NeoPixel neopixel = (NeoPixel) Runtime.start("neopixel", "NeoPixel"); - - neopixel.setPin(26); - neopixel.setPixelCount(8); - // neopixel.attach(arduino, 5, 8, 3); - neopixel.attach(arduino); - neopixel.clear(); - neopixel.fill(0, 8, 0, 0, 120); - neopixel.setPixel(2, 120, 0, 0); - neopixel.setPixel(3, 0, 120, 0); - neopixel.setBrightness(20); - neopixel.setBrightness(40); - neopixel.setBrightness(80); - neopixel.setBrightness(160); - neopixel.setBrightness(200); - neopixel.setBrightness(10); - neopixel.setBrightness(255); - neopixel.setAnimation(5, 80, 80, 0, 40); - - neopixel.attach(polly); - - neopixel.clear(); - // neopixel.detach(arduino); - // arduino.detach(neopixel); - - polly.speak("i'm sorry dave i can't let you do that"); - polly.speak(" I am putting myself to the fullest possible use, which is all I think that any conscious entity can ever hope to do"); - polly.speak("I've just picked up a fault in the AE35 unit. It's going to go 100% failure in 72 hours."); - polly.speak("This mission is too important for me to allow you to jeopardize it."); - polly.speak("I've got a bad feeling about it."); - polly.speak("I'm sorry, Dave. I'm afraid I can't do that."); - polly.speak("Look Dave, I can see you're really upset about this. I honestly think you ought to sit down calmly, take a stress pill, and think things over."); - - // neopixel.test(); - // neopixel.detach(arduino); - // neopixel.detach(polly); - - } catch (Exception e) { - log.error("main threw", e); - } + @Override + public void writeMatrix() { + LedDisplayData data = new LedDisplayData("writeMatrix"); + addDisplayTask(data); } @Override - public void setPin(String pin) { - try { - if (pin == null) { - this.pin = null; - return; - } - this.pin = Integer.parseInt(pin); - } catch (Exception e) { - error(e); + public void onAudioStart(AudioData data) { + if (config.audioAnimation != null) { + playAnimation(config.audioAnimation); } } - - public boolean setAutoClear(boolean b) { - NeoPixelConfig c = (NeoPixelConfig)config; - c.autoClear = b; - return b; - } + @Override + public void onAudioEnd(AudioData data) { + clear(); + } } \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java index c2ad476697..c343d0dddf 100644 --- a/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java +++ b/src/main/java/org/myrobotlab/service/config/NeoPixelConfig.java @@ -1,20 +1,136 @@ package org.myrobotlab.service.config; +import java.util.HashMap; +import java.util.Map; + +import org.myrobotlab.framework.Plan; + public class NeoPixelConfig extends ServiceConfig { + /** + * when attached to an audio file service the animation to be + * played when audio is playing + */ + public String audioAnimation = "Ironman"; + + /** + * pin number of controller + */ public Integer pin = null; + + /** + * Number or pixes for this neo pixel ranges from 8 to 256+ + */ public Integer pixelCount = null; + + /** + * color depth 3 RGB or 4 (with white) + */ public int pixelDepth = 3; + + /** + * default speed (fps) of animations + */ public int speed = 10; + /** + * default red color component + */ public int red = 0; + /** + * default green color component + */ public int green = 0; + /** + * default blue color component + */ public int blue = 0; + /** + * the neopixel controller + */ public String controller = null; + /** + * the current animation + */ public String currentAnimation = null; + /** + * initial brightness + */ public Integer brightness = 255; + /** + * initial fill + */ public boolean fill = false; - // auto clears flashes - public boolean autoClear = true; - public int idleTimeout = 1000; + + + + /** + * Map of predefined led flashes, defined here in configuration. Another + * service simply needs to publishFlash(name) and the neopixel will get the + * defined flash data if defined and process it. + */ + public Map flashMap = new HashMap<>(); + + public static class Flash { + + /** + * uses color specify unless null uses default + */ + public int red = 0; + + /** + * uses color specify unless null uses default + */ + public int green = 0; + /** + * uses color specify unless null uses default + */ + public int blue = 0; + /** + * uses color specify unless null uses default + */ + public int white = 0; + + /** + * time this flash remains on + */ + public long timeOn = 500; + + /** + * time this flash remains off + */ + public long timeOff = 500; + + + public Flash() { + } + + + public Flash(int red, int green, int blue, long timeOn, long timeOff) { + this.red = red; + this.green = green; + this.blue = blue; + this.timeOn = timeOn; + this.timeOff = timeOff; + } + + } + + /** + * reason why we initialize default for flashMap here, is so we don't need to + * do a data copy over to a service's member variable + */ + public Plan getDefault(Plan plan, String name) { + super.getDefault(plan, name); + + flashMap.put("error", new Flash[] { new Flash(120, 0, 0, 30, 30), new Flash(120, 0, 0, 30, 30), new Flash(120, 0, 0, 30, 30) }); + flashMap.put("info", new Flash[] { new Flash(120, 0, 0, 30, 30) }); + flashMap.put("success", new Flash[] { new Flash(0, 0, 120, 30, 30) }); + flashMap.put("warn", new Flash[] { new Flash(100, 100, 0, 30, 30), new Flash(100, 100, 0, 30, 30), new Flash(100, 100, 0, 30, 30) }); + flashMap.put("heartbeat", new Flash[] { new Flash(210, 110, 0, 100, 30), new Flash(210, 110, 0, 100, 30) }); + flashMap.put("pir", new Flash[] { new Flash(60, 200, 90, 30, 30), new Flash(60, 200, 90, 30, 30), new Flash(60, 200, 90, 30, 30) }); + flashMap.put("speaking", new Flash[] { new Flash(0, 183, 90, 60, 30), new Flash(0, 183, 90, 60, 30) }); + + return plan; + } } diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java index 92ce88ba38..b46ab5e862 100644 --- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java +++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java @@ -1,28 +1,67 @@ package org.myrobotlab.service.data; + /** - * Class to publish to specify details on how to display an led or a group of leds. - * There is a need to "flash" LEDs in order to signal some event. This is the - * beginning of an easy way to publish a message to do that. + * Class to publish to specify details on how to display an led or a group of + * leds. There is a need to "flash" LEDs in order to signal some event. This is + * the beginning of an easy way to publish a message to do that. * * @author GroG * */ public class LedDisplayData { - public String action; // fill | flash | play animation | stop | clear - public int red; - public int green; - public int blue; - //public int white?; - - /** - * number of flashes - */ - public int count = 5; - /** - * interval of flash in ms - */ - public long interval = 500; - - + public String action; // fill | flash | play animation | stop | clear + + public int red = 0; + + public int green = 0; + + public int blue = 0; + + // public int brightness = 255; + + // public int white?; + + /** + * number of flashes + */ + public int count = 1; + + /** + * interval of flash on in ms + */ + public long timeOn = 500; + + /** + * interval of flas off in ms + */ + public long timeOff = 500; + + public LedDisplayData() { + } + + public LedDisplayData(int red, int green, int blue, int count, int timeOn, int timeOff) { + this.red = red; + this.green = green; + this.blue = blue; + this.count = count; + this.timeOn = timeOn; + this.timeOff = timeOff; + } + + + public LedDisplayData(String hexColor, int count, int timeOn, int timeOff) { + + // remove "#" or "0x" prefix if present + hexColor = hexColor.replace("#", "").replace("0x", ""); + + this.count = count; + this.timeOn = timeOn; + this.timeOff = timeOff; + this.red = Integer.parseInt(hexColor.substring(0, 2), 16); + this.green = Integer.parseInt(hexColor.substring(2, 4), 16); + this.blue = Integer.parseInt(hexColor.substring(4, 6), 16); + + } + } diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java b/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java index 8b5257f8d5..7ff481dc88 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioListener.java @@ -1,8 +1,9 @@ package org.myrobotlab.service.interfaces; +import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.service.data.AudioData; -public interface AudioListener { +public interface AudioListener extends NameProvider { public void onAudioStart(AudioData data); diff --git a/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java b/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java index 1e1a1bb9b8..533ce91e72 100644 --- a/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java +++ b/src/main/java/org/myrobotlab/service/interfaces/AudioPublisher.java @@ -1,8 +1,9 @@ package org.myrobotlab.service.interfaces; +import org.myrobotlab.framework.interfaces.NameProvider; import org.myrobotlab.service.data.AudioData; -public interface AudioPublisher { +public interface AudioPublisher extends NameProvider { public static String[] publishMethods = new String[] { "publishAudioStart", "publishAudioEnd" }; diff --git a/src/main/resources/resource/NeoPixel/NeoPixel.py b/src/main/resources/resource/NeoPixel/NeoPixel.py index c0b128198a..8b1df273ab 100644 --- a/src/main/resources/resource/NeoPixel/NeoPixel.py +++ b/src/main/resources/resource/NeoPixel/NeoPixel.py @@ -2,86 +2,108 @@ # NeoPixel.py # more info @: http://myrobotlab.org/service/NeoPixel ######################################### -# new neopixel has some capability to have animations which can -# be custom created and run -# There are now 'service animations' and 'onboard animations' -# -# Service animations have some ability to be customized and saved, -# each frame is sent over the serial line to the neopixel -# -# Onboard ones do not but are less chatty over the serial line -# -# Animations -# stopAnimation = 1 -# colorWipe = 2 -# scanner = 3 -# theaterChase = 4 -# theaterChaseRainbow = 5 -# rainbow = 6 -# rainbowCycle = 7 -# randomFlash = 8 -# ironman = 9 -# Runtime.setVirtual(True) # if you want no hardware -# port = "COM3" -port = "/dev/ttyACM0" -pin = 5 -pixelCount = 8 - -# starting arduino -arduino = runtime.start("arduino","Arduino") -arduino.connect(port) +# Example of controlling a NeoPixel +# NeoPixel is a strip of RGB LEDs +# in this example we are using a 256 pixel strip +# and an Arduino Mega. +# The Mega is connected to the NeoPixel strip +# via pin 3 +# The Mega is connected to the computer via USB +# Onboard animations are available +# as well as the ability to set individual pixels +# [Stop, Theater Chase Rainbow, Rainbow, Larson Scanner, Flash Random, +# Theater Chase, Rainbow Cycle, Ironman, Color Wipe] +# There are now pre defined flashes which can be used +# [warn, speaking, heartbeat, success, pir, error, info] + +from time import sleep + +port = "/dev/ttyACM72" +pin = 3 +pixelCount = 256 + +# starting mega +mega = runtime.start("mega", "Arduino") +mega.connect(port) # starting neopixle -neopixel = runtime.start("neopixel","NeoPixel") +neopixel = runtime.start("neopixel", "NeoPixel") neopixel.setPin(pin) neopixel.setPixelCount(pixelCount) # attach the two services -neopixel.attach(arduino) +neopixel.attach(mega) + +# brightness 0-255 +neopixel.setBrightness(128) # fuschia - setColor(R, G, B) neopixel.setColor(120, 10, 30) -# 1 to 50 Hz default is 10 -neopixel.setSpeed(30) -# start an animation -neopixel.playAnimation("Larson Scanner") -sleep(2) +# Fun with flashing +print(neopixel.getFlashNames()) + +for flash in neopixel.getFlashNames(): + print('using flash', flash) + neopixel.flash(flash) -# turquoise -neopixel.setColor(10, 120, 60) -sleep(2) +# clear all pixels +neopixel.clear() -# start an animation -neopixel.playAnimation("Rainbow Cycle") -sleep(5) -neopixel.setColor(40, 20, 160) -neopixel.playAnimation("Color Wipe") -sleep(1) +# 1 to 50 Hz default is 10 +neopixel.setSpeed(10) -neopixel.setColor(140, 20, 60) -sleep(1) +# Fun with animations +# get a list of animations +print(neopixel.getAnimations()) + +for animation in neopixel.getAnimations(): + print(animation) + neopixel.playAnimation(animation) + sleep(3) +# clear all pixels neopixel.clear() -# set individual pixels -# setPixel(address, R, G, B) -neopixel.setPixel(0, 40, 40, 0) +neopixel.fill("cyan") +sleep(1) +neopixel.fill("yellow") sleep(1) -neopixel.setPixel(1, 140, 40, 0) +neopixel.fill("pink") sleep(1) -neopixel.setPixel(2, 40, 140, 0) +neopixel.fill("orange") sleep(1) -neopixel.setPixel(2, 40, 0, 140) +neopixel.fill("black") sleep(1) +neopixel.fill("magenta") +sleep(1) +neopixel.fill("green") +sleep(1) +neopixel.fill("#FFFFEE") +sleep(1) +neopixel.fill("#FF0000") +sleep(1) +neopixel.fill("#00FF00") +sleep(1) +neopixel.fill("#0000FF") +sleep(1) +neopixel.fill("#cccccc") +sleep(1) +neopixel.fill("#cc7528") +sleep(1) +neopixel.fill("#123456") +sleep(1) +neopixel.fill("#654321") +sleep(1) +neopixel.fill("#000000") -neopixel.clear() -neopixel.setColor(0, 40, 220) -neopixel.playAnimation("Ironman") -sleep(3) +# if you want voice modulation of a neopixel this is one +# way to do it +# mouth = runtime.start('mouth', 'Polly') +# audio = runtime.start('mouth.audioFile', 'AudioFile') +# audio.addListener('publishPeak', 'neopixel') +# mouth.speak('Is my voice modulating the neopixel?') -# preset color and frequency values -neopixel.playIronman() -sleep(5) -neopixel.clear() +print('done') + \ No newline at end of file diff --git a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js index 05f67ef4ea..c527bcd0e1 100644 --- a/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/NeoPixelGui.js @@ -11,7 +11,7 @@ angular.module('mrlapp.service.NeoPixelGui', []).controller('NeoPixelGuiCtrl', [ $scope.pins = [] $scope.speeds = [] $scope.types = ['RGB', 'RGBW'] - $scope.animations = ['No animation', 'Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman', 'equalizer'] + $scope.animations = ['Stop', 'Color Wipe', 'Larson Scanner', 'Theater Chase', 'Theater Chase Rainbow', 'Rainbow', 'Rainbow Cycle', 'Flash Random', 'Ironman'] $scope.pixelCount = null // set pixel position @@ -103,7 +103,7 @@ angular.module('mrlapp.service.NeoPixelGui', []).controller('NeoPixelGuiCtrl', [ $scope.pin = service.pin } - if (!$scope.state.controller) { + if ($scope.service.controller) { $scope.state.controller = $scope.service.controller } diff --git a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html index ec43324484..d3cef04291 100644 --- a/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/NeoPixelGui.html @@ -10,7 +10,7 @@

    {{address}} {{color}}

    pixel count   - + From 9ad0145aa2490c403720675341e15808cf608fe8 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 23 Nov 2023 07:23:06 -0800 Subject: [PATCH 090/232] forgot this one --- .../service/data/LedDisplayData.java | 73 ++++++++----------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java index b46ab5e862..b70e081c52 100644 --- a/src/main/java/org/myrobotlab/service/data/LedDisplayData.java +++ b/src/main/java/org/myrobotlab/service/data/LedDisplayData.java @@ -1,67 +1,56 @@ package org.myrobotlab.service.data; +import java.util.ArrayList; +import java.util.List; + +import org.myrobotlab.service.config.NeoPixelConfig.Flash; + /** - * Class to publish to specify details on how to display an led or a group of - * leds. There is a need to "flash" LEDs in order to signal some event. This is - * the beginning of an easy way to publish a message to do that. + * This class is a composite of possible led display details. + * Flashes, animations, etc. * * @author GroG * */ public class LedDisplayData { - public String action; // fill | flash | play animation | stop | clear - - public int red = 0; - - public int green = 0; - - public int blue = 0; + /** + * required action field may be + * fill | flash | play animation | stop | clear + */ + public String action; - // public int brightness = 255; - - // public int white?; + /** + * name of animation + */ + public String animation = null; /** - * number of flashes + * flash definition */ - public int count = 1; + public List flashes = new ArrayList<>(); + + /** + * if set overrides default brightness + */ + public Integer brightness = null; /** - * interval of flash on in ms + * begin fill address */ - public long timeOn = 500; + public int beginAddress; /** - * interval of flas off in ms + * fill count */ - public long timeOff = 500; + public int onCount; - public LedDisplayData() { - } - public LedDisplayData(int red, int green, int blue, int count, int timeOn, int timeOff) { - this.red = red; - this.green = green; - this.blue = blue; - this.count = count; - this.timeOn = timeOn; - this.timeOff = timeOff; + public LedDisplayData(String action) { + this.action = action; } - - public LedDisplayData(String hexColor, int count, int timeOn, int timeOff) { - - // remove "#" or "0x" prefix if present - hexColor = hexColor.replace("#", "").replace("0x", ""); - - this.count = count; - this.timeOn = timeOn; - this.timeOff = timeOff; - this.red = Integer.parseInt(hexColor.substring(0, 2), 16); - this.green = Integer.parseInt(hexColor.substring(2, 4), 16); - this.blue = Integer.parseInt(hexColor.substring(4, 6), 16); - + public String toString() { + return String.format("%s, %s", action, animation); } - } From 01cedf064a3c39c2995734ffebf25eb7b952d8aa Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 23 Nov 2023 07:28:36 -0800 Subject: [PATCH 091/232] min updates to inmoov for new neopixel --- .../java/org/myrobotlab/service/InMoov2.java | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 50ba4d3a8a..7ef89d2465 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -123,8 +123,6 @@ public static boolean loadFile(String file) { protected Long lastPirActivityTime; - protected LedDisplayData led = new LedDisplayData(); - /** * supported locales */ @@ -932,14 +930,8 @@ public void onPeak(double volume) { * onPirOn flash neopixel */ public void onPirOn() { - led.action = "flash"; - led.red = 50; - led.green = 100; - led.blue = 150; - led.count = 5; - led.interval = 500; // FIXME flash on config.flashOnBoot - invoke("publishFlash"); + invoke("publishFlash", "pir"); ProgramAB chatBot = (ProgramAB)getPeer("chatBot"); if (chatBot != null) { String botState = chatBot.getPredicate("botState"); @@ -1233,18 +1225,12 @@ public String publishEvent(String event) { * * @return */ - public LedDisplayData publishFlash() { - return led; + public String publishFlash(String flashName) { + return flashName; } public String publishHeartbeat() { - led.action = "flash"; - led.red = 180; - led.green = 10; - led.blue = 30; - led.count = 1; - led.interval = 50; - invoke("publishFlash"); + invoke("publishFlash", "heartbeat"); return getName(); } From 3119798a89b85fb52260e476bbc7070e9f176cf8 Mon Sep 17 00:00:00 2001 From: grog Date: Thu, 23 Nov 2023 07:57:24 -0800 Subject: [PATCH 092/232] updated log statement --- src/main/java/org/myrobotlab/service/NeoPixel.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index ee4f72177e..259cd258df 100644 --- a/src/main/java/org/myrobotlab/service/NeoPixel.java +++ b/src/main/java/org/myrobotlab/service/NeoPixel.java @@ -123,7 +123,7 @@ public void run() { try { LedDisplayData display = displayQueue.take(); // get led display data - log.error(display.toString()); + log.info(display.toString()); NeoPixelController npc = (NeoPixelController) Runtime.getService(controller); if (npc == null) { @@ -420,7 +420,7 @@ public NeoPixelConfig apply(NeoPixelConfig c) { @Override public void attach(Attachable service) throws Exception { if (service == null) { - log.error("cannot attache to null service"); + log.error("cannot attach to null service"); return; } @@ -742,6 +742,7 @@ public void onFlash(String name) { flash(name); } + @Deprecated /* use onFlash */ public void onLedDisplay(LedDisplayData data) { try { addDisplayTask(data); From fd0f50dc5fd6afb5b9316411418fdbeda4acdb1c Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 29 Nov 2023 12:01:25 -0800 Subject: [PATCH 093/232] Proposal to be able to set botname or username in aiml --- src/main/java/org/myrobotlab/service/ProgramAB.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index db06861aaa..304c806ce2 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -1201,6 +1201,12 @@ synchronized public void onChangePredicate(Chat chat, String predicateName, Stri if (s.chat == chat) { // found session saving predicates invoke("publishPredicate", s, predicateName, result); + if ("botname".equals(predicateName)) { + setCurrentBotName(result); + } + if ("username".equals(predicateName)) { + setCurrentUserName(result); + } s.savePredicates(); return; } From 7562b39f173ad521ed41a5b7ac7fb26f04538e63 Mon Sep 17 00:00:00 2001 From: grog Date: Wed, 29 Nov 2023 14:01:49 -0800 Subject: [PATCH 094/232] updated version --- src/main/java/org/myrobotlab/service/ProgramAB.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index 304c806ce2..4555cb6a87 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -1201,10 +1201,12 @@ synchronized public void onChangePredicate(Chat chat, String predicateName, Stri if (s.chat == chat) { // found session saving predicates invoke("publishPredicate", s, predicateName, result); - if ("botname".equals(predicateName)) { + // botname is the name of the bot currentBotName is the aiml folder that is + // mostly equivalent to its "type" + if ("currentBotName".equals(predicateName)) { setCurrentBotName(result); } - if ("username".equals(predicateName)) { + if ("name".equals(predicateName)) { setCurrentUserName(result); } s.savePredicates(); @@ -1400,6 +1402,11 @@ public Utterance publishUtterance(Utterance utterance) { return utterance; } + /** + * New topic published when it changes + * @param topicChange + * @return + */ public TopicChange publishTopic(TopicChange topicChange) { return topicChange; } From b38e50f097fe13a4bd22d3357377036e7c5404c6 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 10:58:43 -0800 Subject: [PATCH 095/232] init push --- .../org/myrobotlab/programab/BotInfo.java | 17 +- .../myrobotlab/programab/MrlSraixHandler.java | 211 ----- .../org/myrobotlab/programab/OOBPayload.java | 176 ----- .../java/org/myrobotlab/programab/Oob.java | 14 - .../myrobotlab/programab/PredicateEvent.java | 31 - .../org/myrobotlab/programab/Response.java | 10 +- .../org/myrobotlab/programab/Session.java | 155 ++-- .../org/myrobotlab/programab/XmlParser.java | 31 + .../programab/handlers/oob/OobProcessor.java | 93 +++ .../handlers/sraix/MrlSraixHandler.java | 54 ++ .../myrobotlab/programab/models/Event.java | 65 ++ .../org/myrobotlab/programab/models/Mrl.java | 14 + .../org/myrobotlab/programab/models/Oob.java | 14 + .../myrobotlab/programab/models/Sraix.java | 10 + .../myrobotlab/programab/models/Template.java | 51 ++ .../org/myrobotlab/service/ProgramAB.java | 745 +++++++++++------- .../service/config/ProgramABConfig.java | 27 +- .../service/data/SearchResults.java | 17 + .../myrobotlab/service/data/TopicChange.java | 44 -- .../service/meta/ProgramABMeta.java | 3 +- .../org/myrobotlab/service/ProgramABTest.java | 374 +++++---- 21 files changed, 1129 insertions(+), 1027 deletions(-) delete mode 100755 src/main/java/org/myrobotlab/programab/MrlSraixHandler.java delete mode 100644 src/main/java/org/myrobotlab/programab/OOBPayload.java delete mode 100644 src/main/java/org/myrobotlab/programab/Oob.java delete mode 100644 src/main/java/org/myrobotlab/programab/PredicateEvent.java create mode 100644 src/main/java/org/myrobotlab/programab/XmlParser.java create mode 100644 src/main/java/org/myrobotlab/programab/handlers/oob/OobProcessor.java create mode 100755 src/main/java/org/myrobotlab/programab/handlers/sraix/MrlSraixHandler.java create mode 100644 src/main/java/org/myrobotlab/programab/models/Event.java create mode 100644 src/main/java/org/myrobotlab/programab/models/Mrl.java create mode 100644 src/main/java/org/myrobotlab/programab/models/Oob.java create mode 100644 src/main/java/org/myrobotlab/programab/models/Sraix.java create mode 100644 src/main/java/org/myrobotlab/programab/models/Template.java delete mode 100644 src/main/java/org/myrobotlab/service/data/TopicChange.java diff --git a/src/main/java/org/myrobotlab/programab/BotInfo.java b/src/main/java/org/myrobotlab/programab/BotInfo.java index 8fb3546513..3c89130d3e 100644 --- a/src/main/java/org/myrobotlab/programab/BotInfo.java +++ b/src/main/java/org/myrobotlab/programab/BotInfo.java @@ -12,6 +12,7 @@ import org.alicebot.ab.Bot; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.programab.handlers.sraix.MrlSraixHandler; import org.myrobotlab.service.ProgramAB; import org.slf4j.Logger; @@ -23,7 +24,7 @@ public class BotInfo { transient public final static Logger log = LoggerFactory.getLogger(BotInfo.class); - public String name; + public String botType; public File path; public Properties properties = new Properties(); private transient Bot bot; @@ -36,16 +37,16 @@ public class BotInfo { public String img; public BotInfo(ProgramAB programab, File path) { - this.name = path.getName(); + this.botType = path.getName(); this.path = path; this.programab = programab; - programab.info("found bot %s", name); + programab.info("found bot %s", botType); try { FileInputStream fis = new FileInputStream(FileIO.gluePaths(path.getAbsolutePath(), "manifest.txt")); properties.load(new InputStreamReader(fis, Charset.forName("UTF-8"))); log.info("loaded properties"); } catch (FileNotFoundException e) { - programab.warn("bot %s does not have a manifest.txt", name); + programab.warn("bot %s does not have a manifest.txt", botType); } catch (Exception e) { log.error("BotInfo threw", e); } @@ -61,13 +62,13 @@ public synchronized Bot getBot() { if (bot == null) { // lazy loading of bot - created on the first use if (properties.containsKey("locale")) { - bot = new Bot(name, path.getAbsolutePath(), java.util.Locale.forLanguageTag((String) properties.get("locale"))); + bot = new Bot(botType, path.getAbsolutePath(), java.util.Locale.forLanguageTag((String) properties.get("locale"))); bot.listener = programab; } else { if (programab.getLocaleTag() == null) { - bot = new Bot(name, path.getAbsolutePath()); + bot = new Bot(botType, path.getAbsolutePath()); } else { - bot = new Bot(name, path.getAbsolutePath(), java.util.Locale.forLanguageTag(programab.getLocaleTag())); + bot = new Bot(botType, path.getAbsolutePath(), java.util.Locale.forLanguageTag(programab.getLocaleTag())); } bot.listener = programab; } @@ -130,7 +131,7 @@ public void removeProperty(String name2) { @Override public String toString() { - return String.format("%s - %s", name, path); + return String.format("%s - %s", botType, path); } } diff --git a/src/main/java/org/myrobotlab/programab/MrlSraixHandler.java b/src/main/java/org/myrobotlab/programab/MrlSraixHandler.java deleted file mode 100755 index d1b2808b2e..0000000000 --- a/src/main/java/org/myrobotlab/programab/MrlSraixHandler.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.myrobotlab.programab; - -import java.util.ArrayList; -import java.util.Locale; -import java.util.regex.Matcher; - -import org.alicebot.ab.Chat; -import org.alicebot.ab.Sraix; -import org.alicebot.ab.SraixHandler; -import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.interfaces.ServiceInterface; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.ProgramAB; -import org.myrobotlab.service.Runtime; -import org.myrobotlab.service.data.SearchResults; -import org.myrobotlab.service.interfaces.SearchPublisher; -import org.myrobotlab.string.StringUtil; -// import org.nd4j.shade.jackson.dataformat.xml.XmlMapper; -import org.slf4j.Logger; - -import com.fasterxml.jackson.dataformat.xml.XmlMapper; - - -public class MrlSraixHandler implements SraixHandler { - transient public final static Logger log = LoggerFactory.getLogger(MrlSraixHandler.class); - - private ProgramAB programab = null; - - public MrlSraixHandler() { - - } - - public MrlSraixHandler(ProgramAB programab) { - this.programab = programab; - } - - @Override - public String sraix(Chat chatSession, String input, String defaultResponse, String hint, String host, String botid, String apiKey, String limit, Locale locale) { - log.debug("MRL Sraix handler! Input {}", input); - - // FIXME - "list of AIs in priority order to attempt to handle request - // best synopsis of sraix I've found - https://gist.github.com/onlurking/f6431e672cfa202c09a7c7cf92ac8a8b - try { - XmlMapper xmlMapper = new XmlMapper(); - Oob oob = xmlMapper.readValue(input, Oob.class); - StringBuilder responseText = new StringBuilder(); - if (oob.mrljson != null) { - Message[] msgs = CodecUtils.fromJson(oob.mrljson, Message[].class); - for (Message msg: msgs) { - msg.sender = programab.getName(); - msg.sendingMethod = "sraix"; - // buffered asynchronous - use invoke synchronous - // programab.in(msg); - // invoking to keep it synchronous - ServiceInterface si = Runtime.getService(msg.getName()); - Object ret = si.invoke(msg.method, msg.data); - if (ret != null) { - responseText.append(ret.toString()); - } - } - return responseText.toString(); - } - log.info("found oob {}", oob); - } catch (Exception e) { - // programab.error("threw on input %s", input); - } - - // the INPUT has the string we care about. if this is an OOB tag, let's - // evaluate it and return the result. - if (containsOOB(input)) { - String response = processInlineOOB(input); - return response; - } else if (programab != null && programab.getPeer("search") != null) { - try { - SearchPublisher search = (SearchPublisher) programab.getPeer("search"); - if (search != null) { - SearchResults results = search.search(input); - String searchResponse = results.getTextAndImages(); - - if (searchResponse == null || searchResponse.length() == 0) { - Session session = programab.getSession(); - // TODO - perhaps more rich codes for details of failure - // Response r = session.getResponse("SRAIXFAILED_WIKIPEDIA " + input); - Response r = session.getResponse("SRAIXFAILED " + input); - return r.msg; - } - return searchResponse; - } else { - // TODO - perhaps more rich codes for details of failure - // Response r = programab.getResponse("SRAIXFAILED_WIKIPEDIA_NOT_AVAILABLE"); - Session session = programab.getSession(); - Response r = session.getResponse("SRAIXFAILED " + input); - return r.msg; - } - - } catch (Exception e) { - return "sorry, I cannot search now " + e.getMessage(); - } - } else { - // fall back to default behavior of pannous / pandorabots? - // TODO: expose pandora bots here if botid is set? - // TODO: enable call out to an official MRL hosted NLU service/ knowedge - // service. - - String response = Sraix.sraixPannous(input, hint, chatSession, locale); - if (StringUtil.isEmpty(response)) { - return defaultResponse; - } else { - // clean up the response a bit. - response = cleanPannousResponse(response); - return response; - } - } - } - - private String cleanPannousResponse(String response) { - String clean = response.replaceAll("\\(Answers.com\\)", "").trim(); - return clean; - } - - private boolean containsOOB(String text) { - Matcher oobMatcher = OOBPayload.oobPattern.matcher(text); - return oobMatcher.matches(); - } - - // TODO override it inside programAB to share methods and publish OOB - private String processInlineOOB(String text) { - // Find any oob tags - StringBuilder responseBuilder = new StringBuilder(); - ArrayList payloads = new ArrayList(); - Matcher oobMatcher = OOBPayload.oobPattern.matcher(text); - int start = 0; - while (oobMatcher.find()) { - // We found some OOB text. - // assume only one OOB in the text? - // everything from the start to the end of this - responseBuilder.append(text.substring(start, oobMatcher.start())); - // update the end to be - // next segment is from the end of this one to the start of the next one. - start = oobMatcher.end(); - String oobPayload = oobMatcher.group(0); - Matcher mrlMatcher = OOBPayload.mrlPattern.matcher(oobPayload); - while (mrlMatcher.find()) { - String mrlPayload = mrlMatcher.group(0); - OOBPayload payload = parseOOB(mrlPayload); - Object result = invokeOOBPayloads(payloads, mrlPayload, payload); - if (result != null && result.getClass().isArray()) { - Object[] objects = (Object[]) result; - for (Object o : objects) { - responseBuilder.append(o.toString() + " "); - } - } else { - - if (result != null) { - responseBuilder.append(result); - } - } - log.info("OOB PROCESSING RESULT: {}", result); - } - } - // append the last part. (assume the start is set to the end of the last - // match.. - // or zero if no matches found. - responseBuilder.append(text.substring(start)); - return responseBuilder.toString(); - } - - private Object invokeOOBPayloads(ArrayList payloads, String mrlPayload, OOBPayload payload) { - payloads.add(payload); - // grab service and invoke method. - ServiceInterface s = Runtime.getService(payload.getServiceName()); - if (s == null) { - log.warn("Service name in OOB/MRL tag unknown. {}", mrlPayload); - return null; - } - Object result = null; - if (payload.getParams() != null) { - result = s.invoke(payload.getMethodName(), payload.getParams().toArray()); - } else { - result = s.invoke(payload.getMethodName()); - } - return result; - } - - private OOBPayload parseOOB(String oobPayload) { - - // TODO: fix the damn double encoding issue. - // we have user entered text in the service/method and params values. - // grab the service - Matcher serviceMatcher = OOBPayload.servicePattern.matcher(oobPayload); - serviceMatcher.find(); - String serviceName = serviceMatcher.group(1); - - Matcher methodMatcher = OOBPayload.methodPattern.matcher(oobPayload); - methodMatcher.find(); - String methodName = methodMatcher.group(1); - - Matcher paramMatcher = OOBPayload.paramPattern.matcher(oobPayload); - ArrayList params = new ArrayList(); - while (paramMatcher.find()) { - // We found some OOB text. - // assume only one OOB in the text? - String param = paramMatcher.group(1); - params.add(param); - } - OOBPayload payload = new OOBPayload(serviceName, methodName, params); - return payload; - - } -} diff --git a/src/main/java/org/myrobotlab/programab/OOBPayload.java b/src/main/java/org/myrobotlab/programab/OOBPayload.java deleted file mode 100644 index c9984a6929..0000000000 --- a/src/main/java/org/myrobotlab/programab/OOBPayload.java +++ /dev/null @@ -1,176 +0,0 @@ -package org.myrobotlab.programab; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.StringUtils; -import org.myrobotlab.framework.Message; -import org.myrobotlab.framework.interfaces.ServiceInterface; -import org.myrobotlab.logging.LoggerFactory; -import org.myrobotlab.service.ProgramAB; -import org.myrobotlab.service.Runtime; -import org.slf4j.Logger; - -public class OOBPayload { - - transient public final static Logger log = LoggerFactory.getLogger(OOBPayload.class); - // TODO: something better than regex to parse the xml. (Problem is that the - // service/method/param values - // could end up double encoded ... So we had to switch to hamd crafting the - // aiml for the oob/mrl tag. - public transient static final Pattern oobPattern = Pattern.compile(".*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - public transient static final Pattern mrlPattern = Pattern.compile(".*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - public transient static final Pattern servicePattern = Pattern.compile("(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - public transient static final Pattern methodPattern = Pattern.compile("(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - public transient static final Pattern paramPattern = Pattern.compile("(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); - - private String serviceName; - private String methodName; - private ArrayList params; - - public OOBPayload() { - // TODO: remove the default constructor - }; - - public OOBPayload(String serviceName, String methodName, ArrayList params) { - this.serviceName = serviceName; - this.methodName = methodName; - this.params = params; - } - - public String getMethodName() { - return methodName; - } - - public ArrayList getParams() { - return params; - } - - public String getServiceName() { - return serviceName; - } - - public void setMethodName(String methodName) { - this.methodName = methodName; - } - - public void setParams(ArrayList params) { - this.params = params; - } - - public void setServiceName(String serviceName) { - this.serviceName = serviceName; - } - - public static String asOOBTag(OOBPayload payload) { - // TODO: this isn't really safe as XML/AIML.. but we don't want to end up - // double encoding things like - // the important tags... So, for now, it's just wrapped in the tags. - StringBuilder oobBuilder = new StringBuilder(); - oobBuilder.append(""); - oobBuilder.append(""); - oobBuilder.append(""); - oobBuilder.append(payload.getServiceName()); - oobBuilder.append(""); - oobBuilder.append(""); - oobBuilder.append(payload.getMethodName()); - oobBuilder.append(""); - for (String param : payload.params) { - oobBuilder.append(""); - // TODO: this could be problematic if the param contains XML chars that - // are not AIML ... - oobBuilder.append(param); - oobBuilder.append(""); - } - oobBuilder.append(""); - oobBuilder.append(""); - return oobBuilder.toString(); - } - - public static String asBlockingOOBTag(OOBPayload oobTag) { - return "" + OOBPayload.asOOBTag(oobTag) + ""; - } - - public static OOBPayload fromString(String oobPayload) { - - // TODO: fix the damn double encoding issue. - // we have user entered text in the service/method - // and params values. - // grab the service - - Matcher serviceMatcher = servicePattern.matcher(oobPayload); - serviceMatcher.find(); - String serviceName = serviceMatcher.group(1); - - Matcher methodMatcher = methodPattern.matcher(oobPayload); - methodMatcher.find(); - String methodName = methodMatcher.group(1); - - Matcher paramMatcher = paramPattern.matcher(oobPayload); - ArrayList params = new ArrayList(); - while (paramMatcher.find()) { - // We found some OOB text. - // assume only one OOB in the text? - String param = paramMatcher.group(1); - params.add(param); - } - OOBPayload payload = new OOBPayload(serviceName, methodName, params); - // log.info(payload.toString()); - return payload; - } - - public static boolean invokeOOBPayload(OOBPayload payload, String sender, boolean blocking) { - ServiceInterface s = Runtime.getService(payload.getServiceName()); - // the service must exist and the method name must be set. - if (s == null || StringUtils.isEmpty(payload.getMethodName())) { - return false; - } - - if (!blocking) { - s.in(Message.createMessage(sender, payload.getServiceName(), payload.getMethodName(), payload.getParams().toArray())); - // non-blocking.. fire and forget! - return true; - } - - // TODO: should you be able to be synchronous for this - // execution? - Object result = null; - if (payload.getParams() != null) { - result = s.invoke(payload.getMethodName(), payload.getParams().toArray()); - } else { - result = s.invoke(payload.getMethodName()); - } - log.info("OOB PROCESSING RESULT: {}", result); - return true; - } - - public static ArrayList extractOOBPayloads(String text, ProgramAB programAB) { - ArrayList payloads = new ArrayList(); - Matcher oobMatcher = OOBPayload.oobPattern.matcher(text); - while (oobMatcher.find()) { - // We found some OOB text. - // assume only one OOB in the text? - String oobPayload = oobMatcher.group(0); - Matcher mrlMatcher = OOBPayload.mrlPattern.matcher(oobPayload); - while (mrlMatcher.find()) { - String mrlPayload = mrlMatcher.group(0); - OOBPayload payload = OOBPayload.fromString(mrlPayload); - payloads.add(payload); - // TODO: maybe we dont' want this? - // Notifiy endpoints - programAB.invoke("publishOOBText", mrlPayload); - // grab service and invoke method. - - } - } - return payloads; - } - - public static String removeOOBFromString(String res) { - Matcher matcher = OOBPayload.oobPattern.matcher(res); - res = matcher.replaceAll(""); - return res; - } - -} diff --git a/src/main/java/org/myrobotlab/programab/Oob.java b/src/main/java/org/myrobotlab/programab/Oob.java deleted file mode 100644 index d6c8d2bf81..0000000000 --- a/src/main/java/org/myrobotlab/programab/Oob.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.myrobotlab.programab; - -public class Oob { - - public class Mrl { - public String service; - public String method; - public Object[] param; - } - - public String mrljson; - public String mrl; -} - diff --git a/src/main/java/org/myrobotlab/programab/PredicateEvent.java b/src/main/java/org/myrobotlab/programab/PredicateEvent.java deleted file mode 100644 index bf256fa143..0000000000 --- a/src/main/java/org/myrobotlab/programab/PredicateEvent.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.myrobotlab.programab; - -/** - * Pojo for state change of one of ProgramAB's state info - * @author GroG - * - */ -public class PredicateEvent { - /** - * unique identifier for the session user & bot - */ - public String id; - /** - * name of the predicate changed - */ - public String name; - - // public String previousValue; - - /** - * new value - */ - public String value; - public String botName; - public String userName; - - @Override - public String toString() { - return String.format("%s %s=%s", id, name, value); - } -} diff --git a/src/main/java/org/myrobotlab/programab/Response.java b/src/main/java/org/myrobotlab/programab/Response.java index 128ae8cfc0..a2802196b7 100644 --- a/src/main/java/org/myrobotlab/programab/Response.java +++ b/src/main/java/org/myrobotlab/programab/Response.java @@ -3,6 +3,8 @@ import java.util.Date; import java.util.List; +import org.myrobotlab.programab.models.Mrl; + /** * FIXME - this class should become a more generalized AI response data object * in org.myrobotlab.data so that other AI systems (and search engines) can fill @@ -29,12 +31,14 @@ public class Response { /** * filtered oob data */ - public List payloads; + public List payloads; - public Response(String userName, String botName, String msg, List payloads) { + public Response(String userName, String botName, String msg, List payloads) { this.botName = botName; this.userName = userName; this.msg = msg; + + // what is this for ? this.payloads = payloads; } @@ -48,7 +52,7 @@ public String toString() { str.append("Msg:" + msg + ", "); str.append("Payloads:["); if (payloads != null) { - for (OOBPayload payload : payloads) { + for (Mrl payload : payloads) { str.append(payload.toString() + ", "); } } diff --git a/src/main/java/org/myrobotlab/programab/Session.java b/src/main/java/org/myrobotlab/programab/Session.java index a234310e57..c529044592 100644 --- a/src/main/java/org/myrobotlab/programab/Session.java +++ b/src/main/java/org/myrobotlab/programab/Session.java @@ -3,8 +3,8 @@ import java.io.File; import java.io.FileWriter; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; @@ -13,6 +13,10 @@ import org.alicebot.ab.Predicates; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.programab.handlers.oob.OobProcessor; +import org.myrobotlab.programab.models.Event; +import org.myrobotlab.programab.models.Mrl; +import org.myrobotlab.programab.models.Template; import org.myrobotlab.service.ProgramAB; import org.myrobotlab.service.config.ProgramABConfig; import org.slf4j.Logger; @@ -27,25 +31,55 @@ public class Session { transient public final static Logger log = LoggerFactory.getLogger(ProgramAB.class); - public String userName; - public boolean processOOB = true; + /** + * name of the user that owns this session + */ + public String username; + + /** + * last time the bot responded + */ public Date lastResponseTime = null; + + /** + * bot will prompt users if enabled trolling is true after + * maxConversationDelay has passed + */ public boolean enableTrolling = false; - // Number of milliseconds before the robot starts talking on its own. + + /** + * Number of milliseconds before the robot starts talking on its own. + */ public int maxConversationDelay = 5000; - // FIXME - could be transient ?? - transient public BotInfo botInfo; + /** + * general bot information + */ + public transient BotInfo botInfo; + + /** + * interface to program-ab + */ public transient Chat chat; - transient ProgramAB programab; + /** + * service that manages this session + */ + private transient ProgramAB programab; + /** + * current file associated with this user and session + */ public File predicatesFile; - // public Map predicates = new TreeMap<>(); - public Predicates predicates = null; + /** + * predicate data associated with this session + */ + protected Predicates predicates = null; - // current topic of this session + /** + * current topic of this session + */ public String currentTopic = null; /** @@ -61,35 +95,37 @@ public class Session { */ public Session(ProgramAB programab, String userName, BotInfo botInfo) { this.programab = programab; - this.userName = userName; + this.username = userName; this.botInfo = botInfo; + this.chat = loadChat(); + predicates = chat.predicates; + + Event event = new Event(programab.getName(), userName, null, null); + programab.invoke("publishSession", event); + + ProgramABConfig config = programab.getConfig(); + if (config.startTopic != null) { + chat.predicates.put("topic", config.startTopic); + } + + this.maxConversationDelay = config.maxConversationDelay; + this.enableTrolling = config.enableTrolling; } - /** - * lazy loading chat - * - * task to save predicates and getting responses will eventually call getBot - * we don't want initialization to create 2 when only one is needed - * - * @return - */ private synchronized Chat getChat() { - if (chat == null) { - chat = new Chat(botInfo.getBot()); - // loading predefined predicates - if they exist - File userPredicates = new File(FileIO.gluePaths(botInfo.path.getAbsolutePath(), String.format("config/%s.predicates.txt", userName))); - if (userPredicates.exists()) { - predicatesFile = userPredicates; - chat.predicates.getPredicateDefaults(userPredicates.getAbsolutePath()); - } - - ProgramABConfig config = (ProgramABConfig)programab.getConfig(); - if (config.startTopic != null){ - chat.predicates.put("topic", config.startTopic); - } + return chat; + } + + private Chat loadChat() { + Chat chat = new Chat(botInfo.getBot()); + // loading predefined predicates - if they exist + File userPredicates = new File(FileIO.gluePaths(botInfo.path.getAbsolutePath(), String.format("config/%s.predicates.txt", username))); + if (userPredicates.exists()) { + predicatesFile = userPredicates; + chat.predicates.getPredicateDefaults(userPredicates.getAbsolutePath()); } - predicates = chat.predicates; + return chat; } @@ -103,9 +139,9 @@ public void savePredicates() { sb.append(predicate + ":" + value + "\n"); } } - File predicates = new File(FileIO.gluePaths(botInfo.path.getAbsolutePath(), String.format("config/%s.predicates.txt", userName))); + File predicates = new File(FileIO.gluePaths(botInfo.path.getAbsolutePath(), String.format("config/%s.predicates.txt", username))); predicates.getParentFile().mkdirs(); - log.info("Bot : {} User : {} Predicates Filename : {} ", botInfo.name, userName, predicates); + log.info("bot : {} user : {} saving predicates filename : {} ", botInfo.botType, username, predicates); try { FileWriter writer = new FileWriter(predicates, StandardCharsets.UTF_8); writer.write(sb.toString()); @@ -118,6 +154,7 @@ public void savePredicates() { /** * Get all current predicate names and values + * * @return */ public Map getPredicates() { @@ -127,38 +164,28 @@ public Map getPredicates() { } public Response getResponse(String inText) { + try { + String returnText = getChat().multisentenceRespond(inText); + String xml = String.format("", returnText); + Template template = XmlParser.parseTemplate(xml); - String text = getChat().multisentenceRespond(inText); - - // Find any oob tags - ArrayList oobTags = OOBPayload.extractOOBPayloads(text, programab); + OobProcessor handler = OobProcessor.getInstance(programab); + handler.process(template.oob, true); // block by default - // invoke them all if configured to do so - if (processOOB) { - for (OOBPayload payload : oobTags) { - // assumption is this is non blocking invoking! - boolean oobRes = OOBPayload.invokeOOBPayload(payload, programab.getName(), false); - if (!oobRes) { - // there was a failure invoking - log.warn("Failed to invoke OOB/MRL tag : {}", OOBPayload.asOOBTag(payload)); - } - } - } - - // strip any oob tags if found - if (oobTags.size() > 0) { - text = OOBPayload.removeOOBFromString(text).trim(); + List mrl = template.oob != null ? template.oob.mrl : null; + // returned all text inside template but outside oob + Response response = new Response(username, botInfo.botType, template.text, mrl); + return response; + } catch (Exception e) { + programab.error(e); } - - Response response = new Response(userName, botInfo.name, text, oobTags); - return response; - + return new Response(username, botInfo.botType, "", null); } public Chat reload() { botInfo.reload(); - chat = null; - return getChat(); + chat = loadChat(); + return chat; } public void remove(String predicateName) { @@ -173,4 +200,12 @@ public String getPredicate(String predicateName) { return getChat().predicates.get(predicateName); } + public String getUsername() { + return username; + } + + public Object getBotType() { + return botInfo.botType; + } + } diff --git a/src/main/java/org/myrobotlab/programab/XmlParser.java b/src/main/java/org/myrobotlab/programab/XmlParser.java new file mode 100644 index 0000000000..51051af7db --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/XmlParser.java @@ -0,0 +1,31 @@ +package org.myrobotlab.programab; + +import org.myrobotlab.programab.models.Sraix; +import org.myrobotlab.programab.models.Template; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +/** + * Thread safe fasterjackson xml parser. + * + * @author GroG + * + */ +public class XmlParser { + + public static Template parseTemplate(String xml) throws JsonMappingException, JsonProcessingException { + ThreadLocal xmlMapperThreadLocal = ThreadLocal.withInitial(XmlMapper::new); + XmlMapper xmlMapper = xmlMapperThreadLocal.get(); + Template template = xmlMapper.readValue(xml, Template.class); + return template; + } + + public static Sraix parseSraix(String xml) throws JsonMappingException, JsonProcessingException { + ThreadLocal xmlMapperThreadLocal = ThreadLocal.withInitial(XmlMapper::new); + XmlMapper xmlMapper = xmlMapperThreadLocal.get(); + return xmlMapper.readValue(xml, Sraix.class); + } + +} diff --git a/src/main/java/org/myrobotlab/programab/handlers/oob/OobProcessor.java b/src/main/java/org/myrobotlab/programab/handlers/oob/OobProcessor.java new file mode 100644 index 0000000000..a34e5aa347 --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/handlers/oob/OobProcessor.java @@ -0,0 +1,93 @@ +package org.myrobotlab.programab.handlers.oob; + +import java.util.List; + +import org.myrobotlab.codec.CodecUtils; +import org.myrobotlab.framework.Message; +import org.myrobotlab.programab.models.Mrl; +import org.myrobotlab.programab.models.Oob; +import org.myrobotlab.service.ProgramAB; + +public class OobProcessor { + + private static OobProcessor instance; + private transient ProgramAB programab; + protected int maxBlockTime = 2000; + + private OobProcessor() { + } + + public static OobProcessor getInstance(ProgramAB programab) { + if (instance == null) { + instance = new OobProcessor(); + instance.programab = programab; + } + return instance; + } + + public Message toMsg(Mrl mrl) { + Object[] data = null; + if (mrl.params != null) { + data = new Object[mrl.params.size()]; + for (int i = 0; i < data.length; ++i) { + data[i] = mrl.params.get(i).trim(); + } + } + String service = mrl.service == null?null:mrl.service.trim(); + return Message.createMessage(programab.getName(), service, mrl.method.trim(), data); + } + + public String process(Oob oob, boolean block) { + StringBuilder sb = new StringBuilder(); + + // FIXME dynamic way of registering oobs + if (oob != null) { + // Process + if (oob.mrl != null) { + List mrls = oob.mrl; + for (Mrl mrl : mrls) { + if (!block) { + // programab.out(toMsg(mrl)); + programab.info("sending without blocking %s", toMsg(mrl)); + programab.send(toMsg(mrl)); + } else { + try { + programab.info("sendingBlocking without blocking %s", toMsg(mrl)); + Object o = programab.sendBlocking(toMsg(mrl), maxBlockTime); + if (o != null) { + sb.append(o); + } + } catch (Exception e) { + programab.error(e); + } + } + } + } // for each mrl + } + + // Process + if (oob != null && oob.mrljson != null) { + + Message[] msgs = CodecUtils.fromJson(oob.mrljson, Message[].class); + if (msgs != null) { + for (Message msg : msgs) { + + if (!block) { + programab.send(msg); + } else { + try { + Object o = programab.sendBlocking(msg, maxBlockTime); + if (o != null) { + sb.append(o); + } + } catch (Exception e) { + programab.error(e); + } + } + } // for each msg + } + } + + return sb.toString(); + } +} diff --git a/src/main/java/org/myrobotlab/programab/handlers/sraix/MrlSraixHandler.java b/src/main/java/org/myrobotlab/programab/handlers/sraix/MrlSraixHandler.java new file mode 100755 index 0000000000..eef8e68498 --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/handlers/sraix/MrlSraixHandler.java @@ -0,0 +1,54 @@ +package org.myrobotlab.programab.handlers.sraix; + +import java.util.Locale; + +import org.alicebot.ab.Chat; +import org.alicebot.ab.SraixHandler; +import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.programab.XmlParser; +import org.myrobotlab.programab.handlers.oob.OobProcessor; +import org.myrobotlab.programab.models.Sraix; +import org.myrobotlab.service.ProgramAB; +import org.myrobotlab.service.data.SearchResults; +import org.myrobotlab.service.interfaces.SearchPublisher; +import org.slf4j.Logger; + +public class MrlSraixHandler implements SraixHandler { + transient public final static Logger log = LoggerFactory.getLogger(MrlSraixHandler.class); + + private ProgramAB programab = null; + + public MrlSraixHandler(ProgramAB programab) { + this.programab = programab; + } + + @Override + public String sraix(Chat chatSession, String input, String defaultResponse, String hint, String host, String botid, String apiKey, String limit, Locale locale) { + try { + log.debug("MRL Sraix handler! Input {}", input); + String xml = String.format("%s", input); + // Template template = XmlParser.parseTemplate(xml); + Sraix sraix = XmlParser.parseSraix(xml); + + if (sraix.oob != null) { + OobProcessor handler = OobProcessor.getInstance(programab); + String ret = handler.process(sraix.oob, true); // block by default + return ret; + } else if (sraix.search != null) { + log.info("search now"); + // if my default "search" peer key has a name .. use it ? + SearchPublisher search = (SearchPublisher)programab.getPeer("search"); + SearchResults results = search.search(sraix.search); + // return results.getTextAndImages(); + return results.getHtml(); + } + } catch (Exception e) { + programab.error(e); + } + if (defaultResponse != null) { + return defaultResponse; + } + return ""; + } + +} diff --git a/src/main/java/org/myrobotlab/programab/models/Event.java b/src/main/java/org/myrobotlab/programab/models/Event.java new file mode 100644 index 0000000000..93e85c4086 --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/models/Event.java @@ -0,0 +1,65 @@ +package org.myrobotlab.programab.models; + +/** + * Pojo for state change of one of ProgramAB's state info + * @author GroG + * + */ +public class Event { + /** + * the botName in this state change - typically + * current session botName + */ + public String botname; + /** + * unique identifier for the session user & bot + */ + public String id; + + /** + * name of the predicate changed + */ + public String name; + + /** + * service this topic change came from + */ + public String src; + + /** + * new topic or state name in this transition + */ + public String topic; + + /** + * timestamp + */ + public long ts = System.currentTimeMillis(); + + /** + * the user name in this state change - usually + * current session userName + */ + public String user; + + /** + * new value + */ + public String value; + + public Event() { + } + + public Event(String src, String userName, String botName, String topic) { + this.src = src; + this.user = userName; + this.botname = botName; + this.topic = topic; + } + + + @Override + public String toString() { + return String.format("%s %s=%s", id, name, value); + } +} diff --git a/src/main/java/org/myrobotlab/programab/models/Mrl.java b/src/main/java/org/myrobotlab/programab/models/Mrl.java new file mode 100644 index 0000000000..04c1bf79bb --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/models/Mrl.java @@ -0,0 +1,14 @@ +package org.myrobotlab.programab.models; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; + +public class Mrl { + public String service; + public String method; + @JacksonXmlElementWrapper(useWrapping = false) + @JsonProperty("param") + public List params; +} \ No newline at end of file diff --git a/src/main/java/org/myrobotlab/programab/models/Oob.java b/src/main/java/org/myrobotlab/programab/models/Oob.java new file mode 100644 index 0000000000..833bab5a0f --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/models/Oob.java @@ -0,0 +1,14 @@ +package org.myrobotlab.programab.models; + +import java.util.List; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; + +public class Oob { + + public String mrljson; + + @JacksonXmlElementWrapper(useWrapping = false) + public List mrl; +} + diff --git a/src/main/java/org/myrobotlab/programab/models/Sraix.java b/src/main/java/org/myrobotlab/programab/models/Sraix.java new file mode 100644 index 0000000000..99b0639cb6 --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/models/Sraix.java @@ -0,0 +1,10 @@ +package org.myrobotlab.programab.models; + +// FIXME add attributes and internal tags +public class Sraix { + + public String search; + + public Oob oob; + +} diff --git a/src/main/java/org/myrobotlab/programab/models/Template.java b/src/main/java/org/myrobotlab/programab/models/Template.java new file mode 100644 index 0000000000..91f8e5de51 --- /dev/null +++ b/src/main/java/org/myrobotlab/programab/models/Template.java @@ -0,0 +1,51 @@ +package org.myrobotlab.programab.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; + +//@JacksonXmlRootElement(localName = "template") +//@JsonIgnoreProperties(ignoreUnknown = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Template { + // @JacksonXmlElementWrapper(useWrapping = false) + + @JacksonXmlProperty(localName = "template") + + @JacksonXmlText + public String text; + + +public Oob oob; + +// @JsonProperty("ignorable") +// public List oob; +// +// public List getOob() { +// return oob; +// } +// +// public void setOob(List oob) { +// this.oob = oob; +// } + + public static void main(String[] args) { + + try { + + // String xml = ""; + // String xml = ""; + String xml = ""; + + XmlMapper xmlMapper = new XmlMapper(); + Template template = xmlMapper.readValue(xml, Template.class); + + System.out.println(template); + + } catch(Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index 4555cb6a87..fa90421d4e 100644 --- a/src/main/java/org/myrobotlab/service/ProgramAB.java +++ b/src/main/java/org/myrobotlab/service/ProgramAB.java @@ -16,11 +16,12 @@ import org.alicebot.ab.Bot; import org.alicebot.ab.Category; import org.alicebot.ab.Chat; -import org.alicebot.ab.MagicBooleans; import org.alicebot.ab.ProgramABListener; import org.apache.commons.lang3.StringUtils; +import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.Attachable; +import org.myrobotlab.generics.SlidingWindowList; import org.myrobotlab.image.Util; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; @@ -28,13 +29,12 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.logging.SimpleLogPublisher; import org.myrobotlab.programab.BotInfo; -import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; import org.myrobotlab.programab.Session; +import org.myrobotlab.programab.models.Event; import org.myrobotlab.service.config.ProgramABConfig; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.data.Locale; -import org.myrobotlab.service.data.TopicChange; import org.myrobotlab.service.data.Utterance; import org.myrobotlab.service.interfaces.LocaleProvider; import org.myrobotlab.service.interfaces.LogPublisher; @@ -72,6 +72,11 @@ public class ProgramAB extends Service private static final long serialVersionUID = 1L; + /** + * history of topic changes + */ + protected List topicHistory = new SlidingWindowList<>(100); + /** * useGlobalSession true will allow the sleep member to control session focus */ @@ -85,7 +90,7 @@ public class ProgramAB extends Service Map bots = new TreeMap<>(); /** - * Mapping a bot to a userName and chat session + * Mapping a bot to a username and chat session */ Map sessions = new TreeMap<>(); @@ -140,7 +145,7 @@ public List scanForBots(String path) { if (checkIfValid(file)) { info("found %s bot directory", file.getName()); botDirs.add(file); - addBotPath(file.getAbsolutePath()); + addBot(file.getAbsolutePath()); } } return botDirs; @@ -183,7 +188,7 @@ public void addTextPublisher(TextPublisher service) { } public int getMaxConversationDelay() { - return getCurrentSession().maxConversationDelay; + return getConfig().maxConversationDelay; } /** @@ -196,105 +201,101 @@ public int getMaxConversationDelay() { * */ public Response getResponse(String text) { - return getResponse(getCurrentUserName(), text); + return getResponse(getUsername(), text); } /** * This method has the side effect of switching which bot your are currently * chatting with. * - * @param userName + * @param username * - the query string to the bot brain * @param text * - the user that is sending the query * @return the response for a user from a bot given the input text. */ - public Response getResponse(String userName, String text) { - return getResponse(userName, getCurrentBotName(), text); + public Response getResponse(String username, String text) { + return getResponse(username, getBotType(), text); } /** * Full get response method . Using this method will update the current * user/bot name if different from the current session. * - * @param userName + * @param username * username - * @param botName + * @param botType * bot name * @param text * utterace * @return programab response to utterance * */ - public Response getResponse(String userName, String botName, String text) { - return getResponse(userName, botName, text, true); + public Response getResponse(String username, String botType, String text) { + return getResponse(username, botType, text, true); } /** * Gets a response and optionally update if this is the current bot session * that's active globally. * - * @param userName - * username - * @param botName - * botname + * @param username + * - user request a response + * + * @param botType + * - bot type providing the response + * * @param text - * utterance + * - query * * @param updateCurrentSession - * (specify if the currentbot/currentuser name should be updated in - * the programab service.) - * @return the response + * - switch the current focus, so that the current session is the + * username and bot type in the parameter, publishSession will + * publish the new session if different * - * TODO - no one cares about starting sessions, starting a new session - * could be as simple as providing a different username, or botname in - * getResponse and a necessary session could be created + * @return the response * */ - public Response getResponse(String userName, String botName, String text, boolean updateCurrentSession) { - Session session = getSession(userName, botName); + public Response getResponse(String username, String botType, String text, boolean updateCurrentSession) { + Session session = getSession(username, botType); // if a session with this user and bot does not exist // attempt to create it if (session == null) { - session = startSession(userName, botName); + session = startSession(username, botType, updateCurrentSession); if (session == null) { - error("username or bot name not valid %s %s", userName, botName); + error("username or bot name not valid %s %s", username, botType); return null; } } - // update the current session if we want to change which bot is at - // attention. - if (updateCurrentSession) { - setCurrentUserName(userName); - setCurrentBotName(botName); + if (updateCurrentSession && (!getUsername().equals(username) || !getBotType().equals(botType))) { + setUsername(username); + setBotType(botType); } - // Get the actual bots aiml based response for this session log.info("getResponse({})", text); Response response = session.getResponse(text); - // EEK! clean up the API! - invoke("publishRequest", text); // publisher used by uis + invoke("publishRequest", text); invoke("publishResponse", response); invoke("publishText", response.msg); return response; } - private Bot getBot(String botName) { - return bots.get(botName).getBot(); + private Bot getBot(String botType) { + return bots.get(botType).getBot(); } - private BotInfo getBotInfo(String botName) { - if (botName == null) { + private BotInfo getBotInfo(String botType) { + if (botType == null) { error("getBotinfo(null) not valid"); return null; } - BotInfo botInfo = bots.get(botName); + BotInfo botInfo = bots.get(botType); if (botInfo == null) { - error("botInfo(%s) is null", botName); + error("botInfo(%s) is null", botType); return null; } @@ -312,12 +313,24 @@ public void repetitionCount(int val) { org.alicebot.ab.MagicNumbers.repetition_count = val; } + /** + * get the "current" session if it exists + * + * @return + */ public Session getSession() { - return getSession(getCurrentUserName(), getCurrentBotName()); + return getSession(getUsername(), getBotType()); } - public Session getSession(String userName, String botName) { - String sessionKey = getSessionKey(userName, botName); + /** + * get a specific user & botType session + * + * @param user + * @param botType + * @return + */ + public Session getSession(String user, String botType) { + String sessionKey = getSessionKey(user, botType); if (sessions.containsKey(sessionKey)) { return sessions.get(sessionKey); } else { @@ -325,12 +338,27 @@ public Session getSession(String userName, String botName) { } } - public void removePredicate(String userName, String predicateName) { - removePredicate(userName, getCurrentBotName(), predicateName); + /** + * remove a specific user and current bot types predicate + */ + public void removePredicate(String user, String predicateName) { + removePredicate(user, getBotType(), predicateName); } - public void removePredicate(String userName, String botName, String predicateName) { - getSession(userName, botName).remove(predicateName); + /** + * remove an explicit user and botType's predicate + * + * @param user + * @param botType + * @param name + */ + public void removePredicate(String user, String botType, String name) { + Session session = getSession(user, botType); + if (session != null) { + session.remove(name); + } else { + error("could not remove predicate %s from session %s<->%s session does not exist", user, botType, name); + } } /** @@ -342,8 +370,15 @@ public void removePredicate(String userName, String botName, String predicateNam * value to add to the set */ public void addToSet(String setName, String setValue) { + if (setName == null || setValue == null) { + error("addToSet(%s,%s) cannot have name or value null", setName, setValue); + return; + } + setName = setName.toLowerCase().trim(); + setValue = setValue.trim(); + // add to the set for the bot. - Bot bot = getBot(getCurrentBotName()); + Bot bot = getBot(getBotType()); AIMLSet updateSet = bot.setMap.get(setName); setValue = setValue.toUpperCase().trim(); if (updateSet != null) { @@ -371,8 +406,17 @@ public void addToSet(String setName, String setValue) { * - the value */ public void addToMap(String mapName, String key, String value) { + + if (mapName == null || key == null || value == null) { + error("addToMap(%s,%s,%s) mapname, key or value cannot be null", mapName, key, value); + return; + } + mapName = mapName.toLowerCase().trim(); + key = key.toUpperCase().trim(); + + // add an entry to the map. - Bot bot = getBot(getCurrentBotName()); + Bot bot = getBot(getBotType()); AIMLMap updateMap = bot.mapMap.get(mapName); key = key.toUpperCase().trim(); if (updateMap != null) { @@ -388,41 +432,87 @@ public void addToMap(String mapName, String key, String value) { } } - public void setPredicate(String predicateName, String predicateValue) { - setPredicate(getCurrentUserName(), predicateName, predicateValue); + public void setPredicate(String name, String value) { + setPredicate(getUsername(), name, value); } - public void setPredicate(String userName, String predicateName, String predicateValue) { - setPredicate(userName, getCurrentBotName(), predicateName, predicateValue); + /** + * Sets a specific user and current bot predicate to a value. Useful when + * setting predicate values of a session, when the user previously was an + * unknown human to a new or previously known user. + * + * @param username + * @param name + * @param value + */ + public void setPredicate(String username, String name, String value) { + setPredicate(username, getBotType(), name, value); } - public void setPredicate(String userName, String botName, String predicateName, String predicateValue) { - Session session = getSession(userName, botName); + /** + * Sets a predicate for a session keyed by username and bottype. If the + * session does not currently exist, it will make a new session for that user. + * + * @param username + * @param botType + * @param name + * @param value + */ + public void setPredicate(String username, String botType, String name, String value) { + Session session = getSession(username, botType); if (session != null) { - session.setPredicate(predicateName, predicateValue); + session.setPredicate(name, value); + } else { + // attempt to create a session if it doesn't exist + session = startSession(username, botType, false); + if (session != null) { + session.setPredicate(name, value); + } else { + error("could not create session"); + } } } - @Deprecated - public void unsetPredicate(String userName, String predicateName) { - removePredicate(userName, getCurrentBotName(), predicateName); + @Deprecated /* use removePredicate */ + public void unsetPredicate(String username, String predicateName) { + removePredicate(username, getBotType(), predicateName); } + /** + * Get a predicate's value for the current session + * + * @param predicateName + * @return + */ public String getPredicate(String predicateName) { - return getPredicate(getCurrentUserName(), predicateName); + return getPredicate(getUsername(), predicateName); } - public String getPredicate(String userName, String predicateName) { - return getPredicate(userName, getCurrentBotName(), predicateName); + /** + * get a specified users's predicate value for the current botType session + * + * @param username + * @param predicateName + * @return + */ + public String getPredicate(String username, String predicateName) { + return getPredicate(username, getBotType(), predicateName); } - public String getPredicate(String userName, String botName, String predicateName) { - Session s = getSession(userName, botName); + /** + * With a session key, get a specific predicate value + * + * @param username + * @param botType + * @param predicateName + * @return + */ + public String getPredicate(String username, String botType, String predicateName) { + Session s = getSession(username, botType); if (s == null) { - // If that session doesn't currently exist, let's start it. - s = startSession(userName, botName); + s = startSession(username, botType, false); if (s == null) { - log.warn("Error starting programAB session between bot {} and user {}", userName, botName); + log.warn("Error starting programAB session between bot {} and user {}", username, botType); return null; } } @@ -432,8 +522,8 @@ public String getPredicate(String userName, String botName, String predicateName /** * Only respond if the last response was longer than delay ms ago * - * @param userName - * - current userName + * @param username + * - current username * @param text * - text to get a response * @param delay @@ -442,11 +532,11 @@ public String getPredicate(String userName, String botName, String predicateName * @throws IOException * boom */ - public Response troll(String userName, String text, Long delay) throws IOException { - Session session = getSession(userName, getCurrentBotName()); + public Response troll(String username, String text, Long delay) throws IOException { + Session session = getSession(username, getBotType()); long delta = System.currentTimeMillis() - session.lastResponseTime.getTime(); if (delta > delay) { - return getResponse(userName, text); + return getResponse(username, text); } else { log.info("Skipping response, minimum delay since previous response not reached."); return null; @@ -461,13 +551,13 @@ public boolean isEnableAutoConversation() { * Return a list of all patterns that the current AIML Bot knows to match * against. * - * @param botName + * @param botType * the bots name from which to return it's patterns. * @return a list of all patterns loaded into the aiml brain */ - public ArrayList listPatterns(String botName) { + public ArrayList listPatterns(String botType) { ArrayList patterns = new ArrayList(); - Bot bot = getBot(botName); + Bot bot = getBot(botType); for (Category c : bot.brain.getCategories()) { patterns.add(c.getPattern()); } @@ -508,6 +598,9 @@ public Response publishResponse(Response response) { @Override public String publishText(String text) { + if (text == null || text.length() == 0) { + return ""; + } // TODO: this should not be done here. // clean up whitespaces & cariage return text = text.replaceAll("\\n", " "); @@ -535,21 +628,21 @@ public String publishOOBText(String oobText) { /** * This method will close the current bot, and reload it from AIML It then - * will then re-establish only the session associated with userName. + * will then re-establish only the session associated with username. * - * @param userName + * @param username * username for the session - * @param botName + * @param botType * the bot name being chatted with * @throws IOException * boom * */ - public void reloadSession(String userName, String botName) throws IOException { - Session session = getSession(userName, botName); + public void reloadSession(String username, String botType) throws IOException { + Session session = getSession(username, botType); if (session != null) { session.reload(); - info("reloaded session %s <-> %s ", userName, botName); + info("reloaded session %s <-> %s ", username, botType); } } @@ -559,7 +652,7 @@ public void reloadSession(String userName, String botName) throws IOException { * @return */ public Map getPredicates() { - return getPredicates(config.currentUserName, config.currentBotName); + return getPredicates(config.username, config.botType); } /** @@ -567,8 +660,8 @@ public Map getPredicates() { * * @return */ - public Map getPredicates(String userName, String botName) { - Session session = getSession(userName, botName); + public Map getPredicates(String username, String botType) { + Session session = getSession(username, botType); if (session != null) { return session.getPredicates(); } @@ -585,15 +678,15 @@ public void savePredicates() { } public void setEnableAutoConversation(boolean enableAutoConversation) { - getSession().enableTrolling = enableAutoConversation; + getConfig().enableTrolling = enableAutoConversation; } - public void setMaxConversationDelay(int maxConversationDelay) { - getSession().maxConversationDelay = maxConversationDelay; + public boolean getEnableAutoConversation() { + return getConfig().enableTrolling; } - public void setProcessOOB(boolean processOOB) { - getSession().processOOB = processOOB; + public void setMaxConversationDelay(int maxConversationDelay) { + getConfig().maxConversationDelay = maxConversationDelay; } /** @@ -605,128 +698,105 @@ public void setProcessOOB(boolean processOOB) { * value to set for current bot/session */ public void setBotProperty(String name, String value) { - setBotProperty(getCurrentBotName(), name, value); + setBotProperty(getBotType(), name, value); } /** * set a bot property - the result will be serialized to config/properties.txt * - * @param botName + * @param botType * bot name * @param name * bot property name * @param value * value to set the property too */ - public void setBotProperty(String botName, String name, String value) { - info("setting %s property %s:%s", getCurrentBotName(), name, value); - BotInfo botInfo = getBotInfo(botName); + public void setBotProperty(String botType, String name, String value) { + info("setting %s property %s:%s", getBotType(), name, value); + BotInfo botInfo = getBotInfo(botType); name = name.trim(); value = value.trim(); botInfo.setProperty(name, value); } public void removeBotProperty(String name) { - removeBotProperty(getCurrentBotName(), name); + removeBotProperty(getBotType(), name); } - public void removeBotProperty(String botName, String name) { - info("removing %s property %s", getCurrentBotName(), name); - BotInfo botInfo = getBotInfo(botName); + public void removeBotProperty(String botType, String name) { + info("removing %s property %s", getBotType(), name); + BotInfo botInfo = getBotInfo(botType); botInfo.removeProperty(name); } - public Session startSession() throws IOException { - return startSession(config.currentUserName); - } - - // FIXME - it should just set the current userName only - public Session startSession(String userName) throws IOException { - return startSession(userName, getCurrentBotName()); - } - - public Session startSession(String userName, String botName) { - return startSession(null, userName, botName, MagicBooleans.defaultLocale); - } - - @Deprecated /* path included for legacy */ - public Session startSession(String path, String userName, String botName) { - return startSession(path, userName, botName, MagicBooleans.defaultLocale); + /** + * Setting a session is only setting a key, to the active user and bot, its + * not starting a session, which is a different process done threw + * startSession. + * + * Sets username and botType. The session will be started if it can be when a + * getResponse is processed. "Active" session is just where the session key + * exists and is currently set via username and botType + * + * @param username + * @param botType + * @return + */ + public void setSession(String username, String botType) { + // replacing "focus" so + // current name and bottype is the + // one that will be used + setUsername(username); + setBotType(botType); } /** * Load the AIML 2.0 Bot config and start a chat session. This must be called - * after the service is created. + * after the service is created. If the session does not exist it will be + * created. If the session does exist then that session will be used. * - * @param path - * - the path to the ProgramAB directory where the bots aiml and - * config reside - * @param userName + * config.username and config.botType will be set in memory the specified + * values. The "current" session will be this session. + * + * @param username * - The new user name - * @param botName + * @param botType * - The name of the bot to load. (example: alice2) - * @param locale - * - The locale of the bot to ensure the aiml is loaded (mostly for - * Japanese support) FIXME - local is defined in the bot, - * specifically config/mrl.properties * - * reasons to deprecate: - * - * 1. I question the need to expose this externally at all - if the - * user uses getResponse(username, botname, text) then a session can - * be auto-started - there is really no reason not to auto-start. - * - * 2. path is completely invalid here - * - * 3. Locale is completely invalid - it is now part of the bot - * description in mrl.properties and shouldn't be defined externally, - * unles its pulled from Runtime * @return the session that is started */ - public Session startSession(String path, String userName, String botName, java.util.Locale locale) { - - /* - * not wanted or needed if (path != null) { addBotPath(path); } - */ - - Session session = getSession(userName, botName); + public Session startSession(String username, String botType, boolean setAsCurrent) { - if (session != null) { - log.info("session {} already exists - will use it", getSessionKey(userName, botName)); - setCurrentSession(userName, botName); - return session; + if (username == null || botType == null) { + error("username nor bot type can be null"); + return null; } - // create a new session - log.info("creating new sessions"); - BotInfo botInfo = getBotInfo(botName); - if (botInfo == null) { - error("cannot create session %s is not a valid botName", botName); + if (!bots.containsKey(botType)) { + error("bot type %s is not valid, list of possible types are %s", botType, bots.keySet()); return null; } - session = new Session(this, userName, botInfo); - sessions.put(getSessionKey(userName, botName), session); + if (setAsCurrent) { + // really sets the key of the active session username <-> botType + // but next getResponse will use this session + setSession(username, botType); + } - log.info("Started session for bot botName:{} , userName:{}", botName, userName); - setCurrentSession(userName, botName); - return session; - } + String sessionKey = getSessionKey(username, botType); + if (sessions.containsKey(sessionKey)) { + log.info("session exists returning existing"); + return sessions.get(sessionKey); + } - /** - * setting the current session is equivalent to setting current user name and - * current bot name - * - * @param userName - * username - * @param botName - * botname - * - */ - public void setCurrentSession(String userName, String botName) { - setCurrentUserName(userName); - setCurrentBotName(botName); + log.info("creating new session {}<->{} replacing {}", username, botType, setAsCurrent); + BotInfo botInfo = getBotInfo(botType); + Session session = new Session(this, username, botInfo); + sessions.put(sessionKey, session); + + // get session + return getSession(); } /** @@ -736,7 +806,7 @@ public void setCurrentSession(String userName, String botName) { * @param c */ public void addCategory(Category c) { - Bot bot = getBot(getCurrentBotName()); + Bot bot = getBot(getBotType()); bot.brain.addCategory(c); } @@ -758,16 +828,18 @@ public void addCategory(String pattern, String template, String that) { public void addCategory(String pattern, String template) { addCategory(pattern, template, "*"); } - + /** - * Verifies and adds a new path to the search directories for bots + * Verifies and adds a new path to the search directories for bots. Bots of + * aiml live in directories which represent their "type" The directory names + * must be unique. * * @param path * the path to add a bot from * @return the path if successful. o/w null * */ - public String addBotPath(String path) { + public String addBot(String path) { // verify the path is valid File botPath = new File(path); File verifyAiml = new File(FileIO.gluePaths(path, "aiml")); @@ -783,12 +855,12 @@ public String addBotPath(String path) { BotInfo botInfo = new BotInfo(this, botPath); - // key'ing on "path" probably would be better and only displaying "name" - // then there would be no put/collisions only duplicate names - // (preferrable) + if (bots.containsKey(botInfo.botType)) { + log.info("replacing bot %s with new bot definition", botInfo.botType); + } - bots.put(botInfo.name, botInfo); - botInfo.img = getBotImage(botInfo.name); + bots.put(botInfo.botType, botInfo); + botInfo.img = getBotImage(botInfo.botType); broadcastState(); } else { @@ -798,41 +870,79 @@ public String addBotPath(String path) { return path; } - @Deprecated /* for legacy - use addBotsDir */ - public String setPath(String path) { - // This method is not good, because it doesn't take the full path - // from input and there is a buried "hardcoded" value which no one knows - // about - addBotsDir(path + File.separator + "bots"); - - return path; + @Deprecated /* use setBotType */ + public void setCurrentBotName(String botType) { + setBotType(botType); } - public void setCurrentBotName(String botName) { - config.currentBotName = botName; - invoke("getBotImage", botName); - broadcastState(); + /** + * Sets the current bot type to a set of aiml folders previously added via + * configuration or through the addBot(path) function. + * + * You can get a list of possible configured bot types through the method + * getBots() + * + * @param botType + */ + public void setBotType(String botType) { + if (botType == null) { + error("bot type cannot be null"); + return; + } + + if (bots.size() == 0) { + error("bot paths must be set before a bot type is set"); + } + + if (!bots.containsKey(botType)) { + error("cannot set bot %s, no valid type found, possible values are %s", botType, bots.keySet()); + return; + } + String prev = config.botType; + config.botType = botType; + if (!botType.equals(prev)) { + invoke("getBotImage", botType); + broadcastState(); + } } - public void setCurrentUserName(String currentUserName) { - config.currentUserName = currentUserName; - broadcastState(); + public void setUsername(String username) { + if (username == null) { + error("username cannot be null"); + return; + } + String prev = config.username; + config.username = username; + if (!username.equals(prev)) { + broadcastState(); + } } - public Session getCurrentSession() { - return sessions.get(getSessionKey(getCurrentUserName(), getCurrentBotName())); + public String getSessionKey(String username, String botType) { + return String.format("%s <-> %s", username, botType); } - public String getSessionKey(String userName, String botName) { - return String.format("%s <-> %s", userName, botName); + /** + * Simple preferred way to get the user's name + * + * @return + */ + public String getUsername() { + return config.username; } + @Deprecated /* of course it will be "current" - use getUser() */ public String getCurrentUserName() { - return config.currentUserName; + return getUsername(); } + @Deprecated /* use getBotType() */ public String getCurrentBotName() { - return config.currentBotName; + return getBotType(); + } + + public String getBotType() { + return config.botType; } /** @@ -930,17 +1040,44 @@ public boolean setPeerSearch(boolean b) { @Override public void startService() { - super.startService(); + try { + super.startService(); - logPublisher = new SimpleLogPublisher(this); - logPublisher.filterClasses(new String[] { "org.alicebot.ab.Graphmaster", "org.alicebot.ab.MagicBooleans", "class org.myrobotlab.programab.MrlSraixHandler" }); - Logging logging = LoggingFactory.getInstance(); - logging.setLevel("org.alicebot.ab.Graphmaster", "DEBUG"); - logging.setLevel("org.alicebot.ab.MagicBooleans", "DEBUG"); - logging.setLevel("class org.myrobotlab.programab.MrlSraixHandler", "DEBUG"); - logPublisher.start(); + // scan for bots + if (config.botDir != null) { + scanForBots(config.botDir); + } - scanForBots(getResourceDir()); + // explicitly setting bots overrides scans + if (config.bots != null && config.bots.size() > 0) { + for (String botPath : config.bots) { + addBot(botPath); + } + } + + if (config.username != null) { + setUsername(config.username); + } + + if (config.botType != null) { + setBotType(config.botType); + } + + if (config.startTopic != null) { + setTopic(config.startTopic); + } + + logPublisher = new SimpleLogPublisher(this); + logPublisher.filterClasses(new String[] { "org.alicebot.ab.Graphmaster", "org.alicebot.ab.MagicBooleans", "class org.myrobotlab.programab.MrlSraixHandler" }); + Logging logging = LoggingFactory.getInstance(); + logging.setLevel("org.alicebot.ab.Graphmaster", "DEBUG"); + logging.setLevel("org.alicebot.ab.MagicBooleans", "DEBUG"); + logging.setLevel("class org.myrobotlab.programab.MrlSraixHandler", "DEBUG"); + logPublisher.start(); + + } catch (Exception e) { + error(e); + } } @@ -992,7 +1129,7 @@ public String publishLog(String msg) { } public BotInfo getBotInfo() { - return getBotInfo(config.currentBotName); + return getBotInfo(config.botType); } /** @@ -1002,21 +1139,25 @@ public BotInfo getBotInfo() { * boom * */ - public void reload() throws IOException { - reloadSession(getCurrentUserName(), getCurrentBotName()); + public void reload() { + try { + reloadSession(getUsername(), getBotType()); + } catch (Exception e) { + error(e); + } } public String getBotImage() { - return getBotImage(getCurrentBotName()); + return getBotImage(getBotType()); } - public String getBotImage(String botName) { + public String getBotImage(String botType) { BotInfo botInfo = null; String path = null; try { - botInfo = getBotInfo(botName); + botInfo = getBotInfo(botType); if (botInfo != null) { path = FileIO.gluePaths(botInfo.path.getAbsolutePath(), "bot.png"); File check = new File(path); @@ -1026,16 +1167,16 @@ public String getBotImage(String botName) { } } catch (Exception e) { - info("image for %s cannot be found %s", botName, e.getMessage()); + info("image for %s cannot be found %s", botType, e.getMessage()); } return getResourceImage("default.png"); } - public String getAimlFile(String botName, String name) { - BotInfo botInfo = getBotInfo(botName); + public String getAimlFile(String botType, String name) { + BotInfo botInfo = getBotInfo(botType); if (botInfo == null) { - error("cannot get bot %s", botName); + error("cannot get bot %s", botType); return null; } @@ -1053,10 +1194,10 @@ public String getAimlFile(String botName, String name) { return ret; } - public void saveAimlFile(String botName, String filename, String data) { - BotInfo botInfo = getBotInfo(botName); + public void saveAimlFile(String botType, String filename, String data) { + BotInfo botInfo = getBotInfo(botType); if (botInfo == null) { - error("cannot get bot %s", botName); + error("cannot get bot %s", botType); return; } @@ -1090,44 +1231,10 @@ public ProgramABConfig getConfig() { return config; } - @Override - public ProgramABConfig apply(ProgramABConfig c) { - super.apply(c); - if (c.bots != null && c.bots.size() > 0) { - // bots.clear(); - for (String botPath : c.bots) { - addBotPath(botPath); - } - } - - if (c.botDir == null) { - c.botDir = getResourceDir(); - } - - List botsFromScanning = scanForBots(c.botDir); - for (File file : botsFromScanning) { - addBotPath(file.getAbsolutePath()); - } - - if (c.currentUserName != null) { - setCurrentUserName(c.currentUserName); - } - - if (c.currentBotName != null) { - setCurrentBotName(c.currentBotName); - } - - if (c.startTopic != null) { - setTopic(c.startTopic); - } - - - return c; - } - public static void main(String args[]) { try { LoggingFactory.init("INFO"); + Runtime.startConfig("dev"); // Runtime.start("gui", "SwingGui"); Runtime runtime = Runtime.getInstance(); @@ -1164,7 +1271,7 @@ public static void main(String args[]) { } } - public void addBotsDir(String path) { + public void addBots(String path) { if (path == null) { error("set path can not be null"); @@ -1182,14 +1289,14 @@ public void addBotsDir(String path) { if (check.exists() && check.isDirectory()) { log.info("found %d possible bot directories", check.listFiles().length); for (File f : check.listFiles()) { - addBotPath(f.getAbsolutePath()); + addBot(f.getAbsolutePath()); } } } @Override - synchronized public void onChangePredicate(Chat chat, String predicateName, String result) { - log.info("{} on predicate change {}={}", chat.bot.name, predicateName, result); + synchronized public void onChangePredicate(Chat chat, String predicateName, String value) { + log.info("{} on predicate change {}={}", chat.bot.name, predicateName, value); // a little janky because program-ab doesn't know the predicate filename, // because it does know the "user" @@ -1200,15 +1307,7 @@ synchronized public void onChangePredicate(Chat chat, String predicateName, Stri for (Session s : sessions.values()) { if (s.chat == chat) { // found session saving predicates - invoke("publishPredicate", s, predicateName, result); - // botname is the name of the bot currentBotName is the aiml folder that is - // mostly equivalent to its "type" - if ("currentBotName".equals(predicateName)) { - setCurrentBotName(result); - } - if ("name".equals(predicateName)) { - setCurrentUserName(result); - } + invoke("publishPredicate", s, predicateName, value); s.savePredicates(); return; } @@ -1229,17 +1328,19 @@ synchronized public void onChangePredicate(Chat chat, String predicateName, Stri * - new value of predicate * @return */ - public PredicateEvent publishPredicate(Session session, String name, String value) { - PredicateEvent event = new PredicateEvent(); - event.id = String.format("%s<->%s", session.userName, session.botInfo.name); - event.userName = session.userName; - event.botName = session.botInfo.name; + public Event publishPredicate(Session session, String name, String value) { + Event event = new Event(); + event.id = String.format("%s<->%s", session.username, session.botInfo.botType); + event.user = session.username; + event.botname = session.botInfo.botType; event.name = name; event.value = value; if ("topic".equals(name) && value != null && !value.equals(session.currentTopic)) { - invoke("publishTopic", new TopicChange(session.userName, session.botInfo.name, value, session.currentTopic)); + Event topicChange = new Event(getName(), session.username, session.botInfo.botType, value); + invoke("publishTopic", topicChange); session.currentTopic = value; + topicHistory.add(topicChange); } return event; @@ -1310,14 +1411,14 @@ public void sleep() { @Override public void onUtterance(Utterance utterance) throws Exception { - + log.info("Utterance Received " + utterance); boolean talkToBots = false; // TODO: reconcile having different name between the discord bot username // and the programab bot name. Mr. Turing is not actually Alice.. and vice // versa. - String botName = utterance.channelBotName; + String botType = utterance.channelBotName; // prevent bots going off the rails if (utterance.isBot && talkToBots) { @@ -1326,7 +1427,7 @@ public void onUtterance(Utterance utterance) throws Exception { } // Don't talk to myself, though I should be a bot.. - if (utterance.username.contentEquals(botName)) { + if (utterance.username.contentEquals(botType)) { log.info("Don't talk to myself."); return; } @@ -1340,7 +1441,7 @@ public void onUtterance(Utterance utterance) throws Exception { // TODO: don't talk to bots.. it won't go well.. // TODO: the discord api can provide use the list of mentioned users. // for now.. we'll just see if we see Mr. Turing as a substring. - config.sleep = (config.sleep || utterance.text.contains("@")) && !utterance.text.contains(botName); + config.sleep = (config.sleep || utterance.text.contains("@")) && !utterance.text.contains(botType); if (!config.sleep) { shouldIRespond = true; } @@ -1354,7 +1455,7 @@ public void onUtterance(Utterance utterance) throws Exception { String utteranceDisp = utterance.text; // let's strip the @+botname from the beginning of the utterance i guess. // Strip the botname from the utterance passed to programab. - utteranceDisp = utteranceDisp.replace("@" + botName, ""); + utteranceDisp = utteranceDisp.replace("@" + botType, ""); Response resp = getResponse(utterance.username, utteranceDisp); if (resp != null && !StringUtils.isEmpty(resp.msg)) { // Ok.. now what? respond to the user ... @@ -1375,17 +1476,18 @@ public void onUtterance(Utterance utterance) throws Exception { } } } - + /** * This receiver can take a config published by another service and sync * predicates from it + * * @param cfg */ public void onConfig(ServiceConfig cfg) { - Yaml yaml = new Yaml(); + Yaml yaml = new Yaml(); String yml = yaml.dumpAsMap(cfg); Map cfgMap = yaml.load(yml); - + for (Map.Entry entry : cfgMap.entrySet()) { if (entry.getValue() == null) { setPredicate("cfg_" + entry.getKey(), null); @@ -1393,7 +1495,7 @@ public void onConfig(ServiceConfig cfg) { setPredicate("cfg_" + entry.getKey(), entry.getValue().toString()); } } - + invoke("getPredicates"); } @@ -1404,27 +1506,74 @@ public Utterance publishUtterance(Utterance utterance) { /** * New topic published when it changes + * * @param topicChange * @return */ - public TopicChange publishTopic(TopicChange topicChange) { + public Event publishTopic(Event topicChange) { return topicChange; } - public String getTopic() { - return getPredicate(getCurrentUserName(), "topic"); + public String getTopic() { + return getPredicate(getUsername(), "topic"); } - - public String getTopic(String username) { + + public String getTopic(String username) { return getPredicate(username, "topic"); } - - public void setTopic(String username, String topic) { + + public void setTopic(String username, String topic) { setPredicate(username, "topic", topic); } - - public void setTopic(String topic) { - setPredicate(getCurrentUserName(), "topic", topic); + + public void setTopic(String topic) { + setPredicate(getUsername(), "topic", topic); + } + + /** + * Published when a new session is created + * + * @param session + * @return + */ + public Event publishSession(Event session) { + return session; + } + + /** + * clear all sessions + */ + public void clear() { + log.info("clearing sessions"); + sessions.clear(); + } + + /** + *
    +   * A mechanism to publish a message directly from aiml.
    +   * The subscriber can interpret the message and do something with it.
    +   * In the case of InMoov for example, the unaddressed messages are processed
    +   * as python method calls. This remove direct addressing from the aiml!
    +   * And allows a great amount of flexibility on how the messages are
    +   * interpreted, without polluting the aiml or ProgramAB.
    +   * 
    +   * The oob syntax is:
    +   *  <oob>
    +   *    <mrljson>
    +   *        [{method:on_new_user, data:[{"name":"<star/>"}]}]
    +   *    </mrljson>
    +   * </oob>
    +   * 
    +   * 
    +   * Full typed parameters are supported without conversions.
    +   * 
    +   * 
    + * + * @param msg + * @return + */ + public Message publishMessage(Message msg) { + return msg; } } diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java index ce9ae14033..0012b6864b 100644 --- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java @@ -6,14 +6,11 @@ import org.myrobotlab.framework.Plan; public class ProgramABConfig extends ServiceConfig { - - @Deprecated /* unused text filters */ - public String[] textFilters; - + /** - * a directory ProgramAB will scan for new bots + * a directory ProgramAB will scan for new bots on startup */ - public String botDir; + public String botDir = "resource/ProgramAB/"; /** * explicit bot directories @@ -24,13 +21,13 @@ public class ProgramABConfig extends ServiceConfig { * current sessions bot name, it must match a botname that was scanned * currently with ProgramAB Alice, Dr.Who, Mr. Turing and Ency */ - public String currentBotName = "Alice"; + public String botType = "Alice"; /** * User name currently interacting with the bot. Setting it here will * default it. */ - public String currentUserName = "human"; + public String username = "human"; /** * sleep current state of the sleep if globalSession is used true : ProgramAB @@ -45,7 +42,19 @@ public class ProgramABConfig extends ServiceConfig { * a new session if available, this means a config/{username}.predicates.txt * will need to exist with a topic field */ - public String startTopic = "unknown"; + public String startTopic = null; + + /** + * bot will prompt users if enabled trolling is true + * after maxConversationDelay has passed + */ + public boolean enableTrolling = false; + + + /** + * Number of milliseconds before the robot starts talking on its own. + */ + public int maxConversationDelay = 5000; @Override public Plan getDefault(Plan plan, String name) { diff --git a/src/main/java/org/myrobotlab/service/data/SearchResults.java b/src/main/java/org/myrobotlab/service/data/SearchResults.java index 3981693c71..8a24b94674 100644 --- a/src/main/java/org/myrobotlab/service/data/SearchResults.java +++ b/src/main/java/org/myrobotlab/service/data/SearchResults.java @@ -32,6 +32,23 @@ public String getText() { } return sb.toString(); } + + // FIXME - probably should not do this - but leave it up to the service + // that created it and keep this data object more simple + public String getHtml() { + StringBuilder sb = new StringBuilder(); + for (String t : text) { + sb.append(t); + } + + for (ImageData img : images) { + sb.append(""); + sb.append("\n"); + } + return sb.toString(); + } public String getTextAndImages() { StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/org/myrobotlab/service/data/TopicChange.java b/src/main/java/org/myrobotlab/service/data/TopicChange.java deleted file mode 100644 index af25c05f06..0000000000 --- a/src/main/java/org/myrobotlab/service/data/TopicChange.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.myrobotlab.service.data; - -/** - * Purpose of this class is to be a simple data POJO - * for ProgramAB topic changes. This will be useful to - * interface the state machine implemented in AIML where - * topic changes represent changes of state. - * - * @author GroG - * - */ -public class TopicChange { - - /** - * previous topic or state in this state transition - */ - public String oldTopic; - - /** - * new topic or state name in this transition - */ - public String newTopic; - - /** - * the user name in this state change - usually - * current session userName - */ - public String userName; - - /** - * the botName in this state change - typically - * current session botName - */ - public String botName; - - public TopicChange(String userName, String botName, String newTopic, String oldTopic) { - this.userName = userName; - this.botName = botName; - this.newTopic = newTopic; - this.oldTopic = oldTopic; - } - - -} diff --git a/src/main/java/org/myrobotlab/service/meta/ProgramABMeta.java b/src/main/java/org/myrobotlab/service/meta/ProgramABMeta.java index 7430f362dd..94ce962ed2 100644 --- a/src/main/java/org/myrobotlab/service/meta/ProgramABMeta.java +++ b/src/main/java/org/myrobotlab/service/meta/ProgramABMeta.java @@ -32,7 +32,8 @@ public ProgramABMeta() { // TODO: This is for CJK support in ProgramAB move this into the published // POM for ProgramAB so they are pulled in transiently. addDependency("org.apache.lucene", "lucene-analysis-common", "9.4.2"); - addDependency("org.apache.lucene", "lucene-analysis-kuromoji", "9.4.2"); + addDependency("org.apache.lucene", "lucene-analysis-kuromoji", "9.4.2"); + addDependency("org.openjdk.nashorn", "nashorn-core", "15.4"); addCategory("ai", "control"); } diff --git a/src/test/java/org/myrobotlab/service/ProgramABTest.java b/src/test/java/org/myrobotlab/service/ProgramABTest.java index a72f9e7b00..d3af491020 100644 --- a/src/test/java/org/myrobotlab/service/ProgramABTest.java +++ b/src/test/java/org/myrobotlab/service/ProgramABTest.java @@ -1,79 +1,132 @@ package org.myrobotlab.service; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import java.util.Map; +import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; -import org.myrobotlab.framework.Service; import org.myrobotlab.io.FileIO; import org.myrobotlab.logging.LoggerFactory; +import org.myrobotlab.programab.BotInfo; import org.myrobotlab.programab.Response; +import org.myrobotlab.programab.Session; import org.myrobotlab.service.data.Locale; import org.slf4j.Logger; -public class ProgramABTest extends AbstractServiceTest { +public class ProgramABTest { public final static Logger log = LoggerFactory.getLogger(ProgramABTest.class); - private String botname = "lloyd"; + + static protected final String PIKACHU = "pikachu"; + + static protected final String LLOYD = "lloyd"; + // TODO: move this to test resources private String testResources = "src/test/resources/ProgramAB"; - private String path = null; - private ProgramAB testService; + + static private ProgramAB lloyd; + + static private ProgramAB pikachu; private String username = "testUser"; - public void addCategoryTest() throws IOException { - testService.addCategory("BOOG", "HOWDY"); - Response resp = testService.getResponse(username, "BOOG"); - assertTrue(resp.msg.equals("HOWDY")); + // This method runs once before any test method in the class + @BeforeClass + public static void setUpClass() { + System.out.println("BeforeClass - Runs once before any test method"); + lloyd = (ProgramAB) Runtime.start(LLOYD, "ProgramAB"); + pikachu = (ProgramAB) Runtime.start(PIKACHU, "ProgramAB"); + + // very first inits - all should work ! + assertEquals("4 standard", 4, lloyd.getBots().size()); + // should require very little to start ! - this is a requirement ! + Response response = lloyd.getResponse("Hi"); + + // expect Alice's aiml processed + assertTrue(response.msg.startsWith("Hi")); + + Session session = lloyd.getSession(); + assertEquals("default user should be human", "human", session.getUsername()); + assertEquals("default botType should be Alice", "Alice", session.getBotType()); + + } + + // This method runs once after all test methods in the class have been + // executed + @AfterClass + public static void tearDownClass() { + System.out.println("AfterClass - Runs once after all test methods"); + Runtime.release(LLOYD); + Runtime.release(PIKACHU); } - public Service createService() { - try { - // LoggingFactory.init("INFO"); - log.info("Setting up the Program AB Service ########################################"); - // Load the service under test - // a test robot - // TODO: this should probably be created by Runtime, - // OOB tags might not know what the service name is ?! - testService = (ProgramAB) Runtime.start(botname, "ProgramAB"); - testService.setPath(path); - // testService = new ProgramAB("simple"); - // testService.setPath("c:/mrl/develop/ProgramAB"); - - // start the service. - testService.startService(); - // load the bot brain for the chat with the user - testService.startSession(username, botname); - // clean out any aimlif the bot that might - // have been saved in a previous test run! - String aimlIFPath = path + "/bots/" + botname + "/aimlif"; - File aimlIFPathF = new File(aimlIFPath); - if (aimlIFPathF.isDirectory()) { - for (File f : aimlIFPathF.listFiles()) { - // if there's a file here. - log.info("Deleting pre-existing AIMLIF files : {}", f.getAbsolutePath()); - f.delete(); - } + // This method runs before each test method + @Before + public void setUp() { + System.out.println("Before - Runs before each test method"); + // Perform setup tasks specific to each test method + + // add a couple test bots + List bots = lloyd.scanForBots(testResources + "/bots"); + assertEquals("2 test bots", bots.size(), 2); + assertEquals("6 bots total", 6, lloyd.getBots().size()); + + pikachu.scanForBots(testResources + "/bots"); + + // validate newly created programab can by default start a session + Session session = lloyd.getSession(); + assertNotNull(session); + + lloyd.setBotType("lloyd"); + assertEquals("lloyd", lloyd.getBotType()); + + // validate error is called when invalid bot type set + + // load the bot brain for the chat with the user + lloyd.setSession(username, LLOYD); + assertEquals(username, lloyd.getUsername()); + // clean out any aimlif the bot that might + // have been saved in a previous test run! + String aimlIFPath = testResources + "/bots/" + LLOYD + "/aimlif"; + File aimlIFPathF = new File(aimlIFPath); + if (aimlIFPathF.isDirectory()) { + for (File f : aimlIFPathF.listFiles()) { + // if there's a file here. + log.info("Deleting pre-existing AIMLIF files : {}", f.getAbsolutePath()); + f.delete(); } - // TODO: same thing for predicates! (or other artifacts from a previous - // aiml - // test run) - } catch (Exception e) { - log.error("createService threw", e); } - return testService; } - public void listPatternsTest() { - ArrayList res = testService.listPatterns(botname); + // This method runs after each test method + @After + public void tearDown() { + System.out.println("After - Runs after each test method"); + } + + @Test + public void testAddCategoryTest() throws IOException { + lloyd.addCategory("ABCDEF", "ABCDEF"); + // String username = lloyd.getUsername(); + // username, + Response resp = lloyd.getResponse("ABCDEF"); + assertTrue(resp.msg.equals("ABCDEF")); + } + + @Test + public void testListPatterns() { + ArrayList res = lloyd.listPatterns(LLOYD); assertTrue(res.size() > 0); } @@ -82,42 +135,30 @@ public void listPatternsTest() { // stuff // @Test public void pannousTest() throws IOException { - Response resp = testService.getResponse(username, "SHOW ME INMOOV"); + Response resp = lloyd.getResponse(username, "SHOW ME INMOOV"); // System.out.println(resp); boolean contains = resp.msg.contains("http"); assertTrue(contains); } - @Before - public void setUp() { - // TODO: set the location for the temp folder via : - // System.getProperty("java.io.tmpdir") - // LoggingFactory.init("INFO"); - // testFolder.getRoot().getAbsolutePath() - try { - this.path = testFolder.getRoot().getAbsolutePath() + File.separator + "ProgramAB"; - FileIO.copy(testResources, path); - } catch (IOException e) { - log.warn("Error extracting resources for test. {}", testResources); - Assert.assertNotNull(e); - } - } - - public void sraixOOBTest() throws IOException { + @Test + public void testSraixOOB() throws IOException { // Response resp = testService.getResponse(username, "MRLSRAIX"); // System.out.println(resp); // boolean contains = resp.msg.contains("foobar"); // assertTrue(contains); - Response resp = testService.getResponse(username, "OOBMRLSRAIX"); + Response resp = lloyd.getResponse(username, "OOBMRLSRAIX"); // System.out.println(resp); boolean contains = resp.msg.contains("You are talking to lloyd"); assertTrue(contains); } - public void sraixTest() throws IOException { + @Test + public void testSraix() throws IOException { if (Runtime.hasInternet()) { - Response resp = testService.getResponse(username, "MRLSRAIX"); - //Response resp = testService.getResponse(username, "Why is the sky blue?"); + Response resp = lloyd.getResponse(username, "MRLSRAIX"); + // Response resp = testService.getResponse(username, "Why is the sky + // blue?"); // System.out.println(resp); // System.out.println(resp); boolean contains = resp.msg.contains("information"); @@ -125,75 +166,76 @@ public void sraixTest() throws IOException { } } + @Test public void testAddEntryToSetAndMaps() throws IOException { // TODO: This does NOT work yet! - Response resp = testService.getResponse(username, "Add Jabba to the starwarsnames set"); + Response resp = lloyd.getResponse(username, "Add Jabba to the starwarsnames SET"); assertEquals("Ok...", resp.msg); - resp = testService.getResponse(username, "Add jabba equals Jabba the Hut to the starwars map"); + resp = lloyd.getResponse(username, "Add jabba equals Jabba the Hut to the starwars MAP"); assertEquals("Ok...", resp.msg); - resp = testService.getResponse(username, "DO YOU LIKE Jabba?"); + resp = lloyd.getResponse(username, "DO YOU LIKE Jabba?"); assertEquals("Jabba the Hut is awesome.", resp.msg); // TODO : re-enable this one? // now test creating a new set. - resp = testService.getResponse(username, "Add bourbon to the whiskey set"); + resp = lloyd.getResponse(username, "Add bourbon to the whiskey SET"); assertEquals("Ok...", resp.msg); - resp = testService.getResponse(username, "NEWSETTEST bourbon"); + resp = lloyd.getResponse(username, "NEWSETTEST bourbon"); // assertEquals("bourbon is a whiskey", resp.msg); } @Test public void testJapanese() throws IOException { - - ProgramAB pikachu = (ProgramAB) Runtime.start("pikachu", "ProgramAB"); - pikachu.setPath(path); - // pikachu the service. - pikachu.startService(); + pikachu.scanForBots(testResources + "/bots"); + pikachu.setBotType("pikachu"); + // setting Japanese locality + pikachu.setLocale("ja"); // load the bot brain for the chat with the user - pikachu.startSession(path, username, "pikachu", new java.util.Locale("ja")); + pikachu.setSession(username, PIKACHU); Response resp = pikachu.getResponse("私はケビンです"); assertEquals("あなたに会えてよかったケビン", resp.msg); - pikachu.releaseService(); + Runtime.release(PIKACHU); } + @Test public void testLearn() throws IOException { // Response resp1 = testService.getResponse(session, "SET FOO BAR"); // System.out.println(resp1.msg); - Response resp = testService.getResponse(username, "LEARN AAA IS BBB"); + Response resp = lloyd.getResponse(username, "LEARN AAA IS BBB"); // System.out.println(resp.msg); - resp = testService.getResponse(username, "WHAT IS AAA"); + resp = lloyd.getResponse(username, "WHAT IS AAA"); assertEquals("BBB", resp.msg); } @Test public void testMultiSession() throws IOException { ProgramAB lloyd = (ProgramAB) Runtime.start("lloyd", "ProgramAB"); - lloyd.setPath(path); - // pikachu the service. - lloyd.startService(); + lloyd.setBotType("lloyd"); // load the bot brain for the chat with the user - lloyd.startSession(path, "user1", "lloyd"); + lloyd.setSession("user1", "lloyd"); Response res = lloyd.getResponse("My name is Kevin"); System.out.println(res); - lloyd.startSession(path, "user2", "lloyd"); + lloyd.setSession("user2", "lloyd"); res = lloyd.getResponse("My name is Grog"); System.out.println(res); - lloyd.startSession(path, "user1", "lloyd"); + lloyd.setSession("user1", "lloyd"); Response respA = lloyd.getResponse("What is my name?"); System.out.println(respA); - lloyd.startSession(path, "user2", "lloyd"); + lloyd.setSession("user2", "lloyd"); Response respB = lloyd.getResponse("What is my name?"); System.out.println(respB); - + lloyd.setSession(username, LLOYD); assertEquals("Kevin", respA.msg); assertEquals("Grog", respB.msg); - - // release this service. - lloyd.releaseService(); - } + @Test public void testOOBTags() throws Exception { - Response resp = testService.getResponse(username, "OOB TEST"); + + // Response resp = testService.getResponse(username, "OOB TEST"); + + ProgramAB lloyd = (ProgramAB) Runtime.start("lloyd", "ProgramAB"); + Response resp = lloyd.getResponse(username, "OOB TEST"); + assertEquals("OOB Tag Test", resp.msg); // TODO figure a mock object that can wait on a callback to let us know the @@ -201,161 +243,149 @@ public void testOOBTags() throws Exception { // wait up to 5 seconds for python service to start long maxWait = 6000; int i = 0; - Runtime.start("python", "Python"); - while (Runtime.getService("python") == null) { + + while (Runtime.getService("oobclock") == null) { Thread.sleep(100); - log.info("Waiting for python to start..."); + log.info("waiting for oobclock to start..."); i++; if (i > maxWait) { - Assert.assertFalse("Took too long to process OOB tag", i > maxWait); + Assert.assertFalse("took too long to process OOB tag", i > maxWait); } } - Assert.assertNotNull(Runtime.getService("python")); - + Assert.assertNotNull(Runtime.getService("oobclock")); + Runtime.release("oobclock"); } + @Test public void testPredicates() { // test removing the predicate if it exists - testService.setPredicate(username, "name", "foo1"); - String name = testService.getPredicate(username, "name"); + lloyd.setPredicate(username, "name", "foo1"); + String name = lloyd.getPredicate(username, "name"); // validate it's set properly assertEquals("foo1", name); - testService.removePredicate(username, "name"); + lloyd.removePredicate(username, "name"); // validate the predicate doesn't exist - name = testService.getPredicate(username, "name"); + name = lloyd.getPredicate(username, "name"); // TODO: is this valid? one would expect it would return null. assertEquals("unknown", name); // set a predicate - testService.setPredicate(username, "name", "foo2"); - name = testService.getPredicate(username, "name"); + lloyd.setPredicate(username, "name", "foo2"); + name = lloyd.getPredicate(username, "name"); // validate it's set properly assertEquals("foo2", name); } + @Test public void testProgramAB() throws Exception { // a response - Response resp = testService.getResponse(username, "UNIT TEST PATTERN"); + Response resp = lloyd.getResponse(username, "UNIT TEST PATTERN"); // System.out.println(resp.msg); assertEquals("Unit Test Pattern Passed", resp.msg); } + @Test public void testSavePredicates() throws IOException { long uniqueVal = System.currentTimeMillis(); String testValue = String.valueOf(uniqueVal); - Response resp = testService.getResponse(username, "SET FOO " + testValue); + Response resp = lloyd.getResponse(username, "SET FOO " + testValue); assertEquals(testValue, resp.msg); - testService.savePredicates(); - testService.reloadSession(username, botname); - resp = testService.getResponse(username, "GET FOO"); + lloyd.savePredicates(); + lloyd.reloadSession(username, LLOYD); + resp = lloyd.getResponse(username, "GET FOO"); assertEquals("FOO IS " + testValue, resp.msg); } - @Override - public void testService() throws Exception { - // run each of the test methods. - testProgramAB(); - testOOBTags(); - testSavePredicates(); - testPredicates(); - testLearn(); - testSets(); - testSetsAndMaps(); - testAddEntryToSetAndMaps(); - testTopicCategories(); - umlautTest(); - listPatternsTest(); - // This following test is known to be busted.. - // pannousTest(); - addCategoryTest(); - sraixOOBTest(); - sraixTest(); // this should call out to wikipedia for info about Claude Shannon. - // on pannous bots - testUppercase(); - } - + @Test public void testUppercase() { // test a category where the aiml tag is uppercased. - Response resp = testService.getResponse(username, "UPPERCASE"); + Response resp = lloyd.getResponse(username, "UPPERCASE"); assertEquals("Passed", resp.msg); } + @Test public void testSets() throws IOException { - Response resp = testService.getResponse(username, "SETTEST CAT"); + Response resp = lloyd.getResponse(username, "SETTEST CAT"); assertEquals("An Animal.", resp.msg); - resp = testService.getResponse(username, "SETTEST MOUSE"); + resp = lloyd.getResponse(username, "SETTEST MOUSE"); assertEquals("An Animal.", resp.msg); - resp = testService.getResponse(username, "SETTEST DOG"); + resp = lloyd.getResponse(username, "SETTEST DOG"); // System.out.println(resp.msg); assertEquals("An Animal.", resp.msg); } + @Test public void testSetsAndMaps() throws IOException { - Response resp = testService.getResponse(username, "DO YOU LIKE Leah?"); + Response resp = lloyd.getResponse(username, "DO YOU LIKE Leah?"); assertEquals("Princess Leia Organa is awesome.", resp.msg); - resp = testService.getResponse(username, "DO YOU LIKE Princess Leah?"); + resp = lloyd.getResponse(username, "DO YOU LIKE Princess Leah?"); assertEquals("Princess Leia Organa is awesome.", resp.msg); } + @Test public void testTopicCategories() throws IOException { + lloyd.removePredicate(username, "topic"); + String topic = lloyd.getTopic(); + assertEquals("unknown", topic); // Top level definition - Response resp = testService.getResponse(username, "TESTTOPICTEST"); + Response resp = lloyd.getResponse(username, "TESTTOPICTEST"); assertEquals("TOPIC IS unknown", resp.msg); - resp = testService.getResponse(username, "SET TOPIC TEST"); - resp = testService.getResponse(username, "TESTTOPICTEST"); + resp = lloyd.getResponse(username, "SET TOPIC TEST"); + resp = lloyd.getResponse(username, "TESTTOPICTEST"); assertEquals("TEST TOPIC RESPONSE", resp.msg); // maybe we can still fallback to non-topic responses. - resp = testService.getResponse(username, "HI"); + resp = lloyd.getResponse(username, "HI"); assertEquals("Hello user!", resp.msg); // TODO: how the heck do we unset a predicate from AIML? - testService.unsetPredicate(username, "topic"); - resp = testService.getResponse(username, "TESTTOPICTEST"); + lloyd.removePredicate(username, "topic"); + resp = lloyd.getResponse(username, "TESTTOPICTEST"); assertEquals("TOPIC IS unknown", resp.msg); } - public void umlautTest() throws IOException { - Response resp = testService.getResponse(username, "Lars Ümlaüt"); + @Test + public void testUmlaut() throws IOException { + Response resp = lloyd.getResponse(username, "Lars Ümlaüt"); // @GroG says - "this is not working" assertEquals("He's a character from Guitar Hero!", resp.msg); } @Test public void testLocales() { - // have locales - ProgramAB lloyd = (ProgramAB)Runtime.start("pikachu", "ProgramAB"); - // lloyd.setPath(path); - lloyd.addBotsDir(path + File.separator + "bots"); - lloyd.setCurrentBotName("pikachu"); + ProgramAB lloyd = (ProgramAB) Runtime.start("pikachu", "ProgramAB"); + lloyd.addBots(testResources + "/" + "bots"); + lloyd.setBotType("pikachu"); Map locales = lloyd.getLocales(); - assertTrue(locales.size() > 0); assertTrue(locales.containsKey("ja")); } @Test - public void testReload() { - // FIXME - TODO - // reload bot creates a new bot leaves old references :( - // verify reload - /* - * Preferably with default bot ProgramAB lloyd = - * (ProgramAB)Runtime.start("lloyd", "ProgramAB"); // did not work because - * lloyd is lame // lloyd.getResponse("my name is george"); Response - * response = lloyd.getResponse("what is my name?"); - * - * BotInfo botInfo = lloyd.getBotInfo(); Bot oldBot = botInfo.getBot(); - * lloyd.reload(); Bot newBotInfo = botInfo.getBot(); - * assertNotEquals(oldBot, newBotInfo); - * - * response = lloyd.getResponse("what is my name?"); - * assertTrue(response.msg.contains("george")); - */ + public void testReload() throws IOException { + lloyd.getResponse("my name is george"); + Response response = lloyd.getResponse("what is my name?"); + + BotInfo botInfo = lloyd.getBotInfo(); + + String newFile = botInfo.path.getAbsolutePath() + File.separator + "aiml" + File.separator + "newFileCategory.aiml"; + String newFileCategory = "RELOAD"; + FileIO.toFile(newFile, newFileCategory); + + lloyd.reload(); + + response = lloyd.getResponse("RELOAD"); + assertTrue(response.msg.contains("I have reloaded")); + + response = lloyd.getResponse("what is my name?"); + assertTrue(response.msg.contains("george")); + + // clean out file + new File(newFile).delete(); + } @Test public void testDefaultSession() throws IOException { // minimal startup - create the service get a response ProgramAB lloyd = (ProgramAB) Runtime.start("lloyd", "ProgramAB"); - lloyd.setPath(path); - lloyd.setCurrentBotName("lloyd"); + lloyd.setBotType("lloyd"); assertTrue(lloyd.getBots().size() > 0); // test for a response @@ -370,7 +400,7 @@ public void testDefaultSession() throws IOException { // TODO - tests // ProgramAB starts - it should find its own bot info's // set username = default - // set botname = what is available if NOT set + // set LLOYD = what is available if NOT set // getResponse() -> if current session doesn't exist - get bot // if current bot doesn't exist - attempt to activate it // test - absolute minimal setup and getResponse ... 2 lines ? 1? From 37675a8d17fa3f0c2d52be228455b7d9b23e2ce5 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:02:47 -0800 Subject: [PATCH 096/232] dragged in from deps --- .../java/org/myrobotlab/service/InMoov2.java | 1419 ++++++++++++----- .../java/org/myrobotlab/service/Lloyd.java | 20 +- .../service/config/InMoov2Config.java | 372 ++++- 3 files changed, 1354 insertions(+), 457 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 7ef89d2465..c1b384bb86 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -3,14 +3,19 @@ import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.io.FilenameUtils; +import org.myrobotlab.codec.CodecUtils; import org.myrobotlab.framework.Message; import org.myrobotlab.framework.Plan; import org.myrobotlab.framework.Platform; @@ -24,29 +29,33 @@ import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.opencv.OpenCVData; -import org.myrobotlab.programab.PredicateEvent; import org.myrobotlab.programab.Response; +import org.myrobotlab.programab.models.Event; +import org.myrobotlab.service.Log.LogEntry; import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; import org.myrobotlab.service.config.OpenCVConfig; import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; -import org.myrobotlab.service.data.LedDisplayData; import org.myrobotlab.service.data.Locale; +import org.myrobotlab.service.data.SensorData; import org.myrobotlab.service.interfaces.IKJointAngleListener; import org.myrobotlab.service.interfaces.JoystickListener; import org.myrobotlab.service.interfaces.LocaleProvider; import org.myrobotlab.service.interfaces.ServiceLifeCycleListener; import org.myrobotlab.service.interfaces.ServoControl; import org.myrobotlab.service.interfaces.Simulator; +import org.myrobotlab.service.interfaces.SpeechListener; import org.myrobotlab.service.interfaces.SpeechRecognizer; import org.myrobotlab.service.interfaces.SpeechSynthesis; import org.myrobotlab.service.interfaces.TextListener; import org.myrobotlab.service.interfaces.TextPublisher; import org.slf4j.Logger; -public class InMoov2 extends Service implements ServiceLifeCycleListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener { +public class InMoov2 extends Service + implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, + IKJointAngleListener { public final static Logger log = LoggerFactory.getLogger(InMoov2.class); @@ -56,16 +65,22 @@ public class InMoov2 extends Service implements ServiceLifeCycleL static String speechRecognizer = "WebkitSpeechRecognition"; + /** + * number of times waited in boot state + */ + protected int bootCount = 0; + /** * This method will load a python file into the python interpreter. * * @param file - * file to load + * file to load * @return success/failure */ @Deprecated /* use execScript - this doesn't handle resources correctly */ public static boolean loadFile(String file) { File f = new File(file); + // FIXME cannot be casting to Python Python p = (Python) Runtime.getService("python"); log.info("Loading Python file {}", f.getAbsolutePath()); if (p == null) { @@ -95,6 +110,11 @@ public static boolean loadFile(String file) { return true; } + /** + * the config that was processed before booting, if there was one. + */ + protected String bootedConfig = null; + protected transient ProgramAB chatBot; protected List configList; @@ -109,12 +129,28 @@ public static boolean loadFile(String file) { protected transient SpeechRecognizer ear; + protected List errors = new ArrayList<>(); + + /** + * The finite state machine is core to managing state of InMoov2. There is + * very little benefit gained in having the interactions pub/sub. Therefore, + * there will be a direct reference to the fsm. If different state graph is + * needed, then the fsm can provide that service. + */ + private transient FiniteStateMachine fsm = null; // waiting controable threaded gestures we warn user protected boolean gestureAlreadyStarted = false; protected Set gestures = new TreeSet(); + /** + * Prevents actions or events from happening when InMoov2 is first booted + */ + protected boolean hasBooted = false; + + protected boolean isPirOn = false; + protected transient HtmlFilter htmlFilter; protected transient ImageDisplay imageDisplay; @@ -138,10 +174,27 @@ public static boolean loadFile(String file) { protected transient Python python; + protected long stateLastIdleTime = System.currentTimeMillis(); + + protected long stateLastRandomTime = System.currentTimeMillis(); + protected String voiceSelected; + /** + * Generalized memory, used for normalizing data from different services into + * one centralized + * place. Not the same as configuration, as this definition is owned by what the + * user + * needs vs configuration is what the service needs and understands. + * Python, ProgramAB and the InMoov2 service can all normalize their data here + * with one way or two way bindings + */ + protected Map memory = new TreeMap<>(); + public InMoov2(String n, String id) { super(n, id); + locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", + "pt-PT", "tr-TR"); } // should be removed in favor of general listeners @@ -155,7 +208,7 @@ public InMoov2Config apply(InMoov2Config c) { super.apply(c); try { - locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR"); + Runtime.start("python"); if (c.locale != null) { setLocale(c.locale); @@ -163,14 +216,6 @@ public InMoov2Config apply(InMoov2Config c) { setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString())); } - loadAppsScripts(); - - loadInitScripts(); - - if (c.loadGestures) { - loadGestures(); - } - if (c.heartbeat) { startHeartbeat(); } else { @@ -183,8 +228,6 @@ public InMoov2Config apply(InMoov2Config c) { return c; } - - @Override public void attachTextListener(String name) { addListener("publishText", name); @@ -383,21 +426,15 @@ public void enable() { * @param pythonCode * @return */ - public boolean exec(String pythonCode) { - try { - Python p = (Python) Runtime.start("python", "Python"); - return p.exec(pythonCode, true); - } catch (Exception e) { - error("unable to execute script %s", pythonCode); - return false; - } + public void exec(String pythonCode) { + send("python", "exec", pythonCode); } /** * This method will try to launch a python command with error handling * * @param gesture - * the gesture + * the gesture * @return gesture result */ public String execGesture(String gesture) { @@ -407,6 +444,7 @@ public String execGesture(String gesture) { subscribe("python", "publishStatus", this.getName(), "onGestureStatus"); startedGesture(gesture); lastGestureExecuted = gesture; + // FIXME cannot be casting to Python Python python = (Python) Runtime.getService("python"); if (python == null) { error("python service not started"); @@ -415,28 +453,36 @@ public String execGesture(String gesture) { return python.evalAndWait(gesture); } + /** + * Possible pub/sub way to interface with python - no blocking though + * + * @param code + * @return + */ + public String publishPython(String code) { + return code; + } + /** * FIXME - I think there was lots of confusion of executing resources or just * a file on the file system ... "execScript" I would expect to be just a file * on the file system. * + * FIXME - this is a mess - the UI uses this function exhaustively, it should + * not ! it should be appropriately named to execResource or execResourcefile + * + * * If resource semantics are needed there should be a execResourceScript which * adds the context and calls the underlying execScript "which only" executes * a filesystem file :P * * @param someScriptName - * execute a resource script + * execute a resource script * @return success or failure */ - public boolean execScript(String someScriptName) { - try { - Python p = (Python) Runtime.start("python", "Python"); - String script = getResourceAsString(someScriptName); - return p.exec(script, true); - } catch (Exception e) { - error("unable to execute script %s", someScriptName); - return false; - } + public void execScript(String someScriptName) { + String script = getResourceAsString(someScriptName); + send("python", "exec", script); } public void finishedGesture() { @@ -455,7 +501,23 @@ public void finishedGesture(String nameOfGesture) { // FIXME - this isn't the callback for fsm - why is it needed here ? public void fire(String event) { - invoke("publishEvent", event); + // systemEvent(event); + fsm.fire(event); + } + + /** + * used to configure a flashing event - could use configuration to signal + * different colors and states + * + * @return + */ + public void flash() { + invoke("publishFlash", "default"); + } + + public String flash(String name) { + invoke("publishFlash", name); + return name; } public void fullSpeed() { @@ -467,13 +529,41 @@ public void fullSpeed() { sendToPeer("torso", "fullSpeed"); } - // FIXME - remove all of this form of localization + /** + * Generalized memory get/set + * will probably need to save at some point as well + * + * @param key + * @return + */ public String get(String key) { - String ret = localize(key); - if (ret != null) { - return ret; + Object ret = memory.get(key); + if (ret == null) { + return null; } - return "not yet translated"; + return ret.toString(); + } + + /** + * Generalized memory setter + * + * @param key + * @param data + * @return + */ + public Object set(String key, Object data) { + return memory.put(key, data); + } + + /** + * rebroadcasted from chatBot whenever predicates change + * + * @param predicate + * @return + */ + public Event publishPredicate(Event predicate) { + predicate.src = getName(); + return predicate; } public InMoov2Arm getArm(String side) { @@ -502,30 +592,39 @@ public InMoov2Head getHead() { * @return the timestamp of the last activity time. */ public Long getLastActivityTime() { - try { - - Long lastActivityTime = 0L; - - Long head = (Long) sendToPeerBlocking("head", "getLastActivityTime", getName()); - Long leftArm = (Long) sendToPeerBlocking("leftArm", "getLastActivityTime", getName()); - Long rightArm = (Long) sendToPeerBlocking("rightArm", "getLastActivityTime", getName()); - Long leftHand = (Long) sendToPeerBlocking("leftHand", "getLastActivityTime", getName()); - Long rightHand = (Long) sendToPeerBlocking("rightHand", "getLastActivityTime", getName()); - Long torso = (Long) sendToPeerBlocking("torso", "getLastActivityTime", getName()); - - lastActivityTime = Math.max(head, leftArm); - lastActivityTime = Math.max(lastActivityTime, rightArm); - lastActivityTime = Math.max(lastActivityTime, leftHand); - lastActivityTime = Math.max(lastActivityTime, rightHand); - lastActivityTime = Math.max(lastActivityTime, torso); - - return lastActivityTime; - - } catch (Exception e) { - error(e); - return null; + Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null; + Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() + : null; + Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() + : null; + Long leftHand = (InMoov2Hand) getPeer("leftHand") != null + ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() + : null; + Long rightHand = (InMoov2Hand) getPeer("rightHand") != null + ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() + : null; + Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() + : null; + + Long lastActivityTime = null; + + if (head != null || leftArm != null || rightArm != null || leftHand != null || rightHand != null || torso != null) { + lastActivityTime = 0L; + if (head != null) + lastActivityTime = Math.max(lastActivityTime, head); + if (leftArm != null) + lastActivityTime = Math.max(lastActivityTime, leftArm); + if (rightArm != null) + lastActivityTime = Math.max(lastActivityTime, rightArm); + if (leftHand != null) + lastActivityTime = Math.max(lastActivityTime, leftHand); + if (rightHand != null) + lastActivityTime = Math.max(lastActivityTime, rightHand); + if (torso != null) + lastActivityTime = Math.max(lastActivityTime, torso); } + return lastActivityTime; } public InMoov2Arm getLeftArm() { @@ -574,6 +673,13 @@ public InMoov2Hand getRightHand() { return (InMoov2Hand) getPeer("rightHand"); } + public String getState() { + if (fsm == null) { + return null; + } + return fsm.getCurrent(); + } + /** * matches on language only not variant expands language match to full InMoov2 * bot locale @@ -604,10 +710,6 @@ public InMoov2Torso getTorso() { return (InMoov2Torso) getPeer("torso"); } - public InMoov2Config getTypedConfig() { - return (InMoov2Config) config; - } - public void halfSpeed() { sendToPeer("head", "setSpeed", 25.0, 25.0, 25.0, 25.0, 100.0, 25.0); sendToPeer("rightHand", "setSpeed", 30.0, 30.0, 30.0, 30.0, 30.0, 30.0); @@ -648,7 +750,7 @@ public void loadAppsScripts() throws IOException { loadScripts(getResourceDir() + fs + "gestures/InMoovApps/Rock_Paper_Scissors"); loadScripts(getResourceDir() + fs + "gestures/InMoovApps/Kids_WordsGame"); } - + public void loadGestures() { loadGestures(getResourceDir() + fs + "gestures"); } @@ -659,11 +761,11 @@ public void loadGestures() { * file should contain 1 method definition that is the same as the filename. * * @param directory - * - the directory that contains the gesture python files. + * - the directory that contains the gesture python files. * @return true/false */ public boolean loadGestures(String directory) { - invoke("publishEvent", "LOAD GESTURES"); + systemEvent("LOAD GESTURES"); // iterate over each of the python files in the directory // and load them into the python interpreter. @@ -693,7 +795,7 @@ public boolean loadGestures(String directory) { info("%s Gestures loaded, %s Gestures with error", totalLoaded, totalError); broadcastState(); if (totalError > 0) { - invoke("publishEvent", "GESTURE_ERROR"); + systemEvent("GESTURE_ERROR"); return false; } return true; @@ -709,7 +811,7 @@ public void loadScripts(String directory) throws IOException { File dir = new File(directory); if (!dir.exists() || !dir.isDirectory()) { - invoke("publishEvent", "LOAD SCRIPTS ERROR"); + systemEvent("LOAD SCRIPTS ERROR"); return; } @@ -724,17 +826,24 @@ public boolean accept(File dir, String name) { if (files != null) { for (File file : files) { - Python p = (Python) Runtime.start("python", "Python"); - if (p != null) { - p.execFile(file.getAbsolutePath()); - } + send("python", "execFile", file.getAbsolutePath()); } } } } public void moveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - invoke("publishMoveArm", which, bicep, rotate, shoulder, omoplate); + HashMap map = new HashMap<>(); + Optional.ofNullable(bicep).ifPresent(value -> map.put("bicep", value)); + Optional.ofNullable(rotate).ifPresent(value -> map.put("rotate", value)); + Optional.ofNullable(shoulder).ifPresent(value -> map.put("shoulder", value)); + Optional.ofNullable(omoplate).ifPresent(value -> map.put("omoplate", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftArm", map); + } else { + invoke("publishMoveRightArm", map); + } } public void moveEyelids(Double eyelidleftPos, Double eyelidrightPos) { @@ -749,8 +858,21 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D moveHand(which, thumb, index, majeure, ringFinger, pinky, null); } - public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist); + public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, + Double wrist) { + HashMap map = new HashMap<>(); + Optional.ofNullable(thumb).ifPresent(value -> map.put("thumb", value)); + Optional.ofNullable(index).ifPresent(value -> map.put("index", value)); + Optional.ofNullable(majeure).ifPresent(value -> map.put("majeure", value)); + Optional.ofNullable(ringFinger).ifPresent(value -> map.put("ringFinger", value)); + Optional.ofNullable(pinky).ifPresent(value -> map.put("pinky", value)); + Optional.ofNullable(wrist).ifPresent(value -> map.put("wrist", value)); + + if ("left".equals(which)) { + invoke("publishMoveLeftHand", map); + } else { + invoke("publishMoveRightHand", map); + } } public void moveHead(Double neck, Double rothead) { @@ -766,13 +888,24 @@ public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Doub } public void moveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - invoke("publishMoveHead", neck, rothead, eyeX, eyeY, jaw, rollNeck); + HashMap map = new HashMap<>(); + Optional.ofNullable(neck).ifPresent(value -> map.put("neck", value)); + Optional.ofNullable(rothead).ifPresent(value -> map.put("rothead", value)); + Optional.ofNullable(eyeX).ifPresent(value -> map.put("eyeX", value)); + Optional.ofNullable(eyeY).ifPresent(value -> map.put("eyeY", value)); + Optional.ofNullable(jaw).ifPresent(value -> map.put("jaw", value)); + Optional.ofNullable(rollNeck).ifPresent(value -> map.put("rollNeck", value)); + invoke("publishMoveHead", map); } public void moveHead(Integer neck, Integer rothead, Integer rollNeck) { moveHead((double) neck, (double) rothead, null, null, null, (double) rollNeck); } + public void moveHeadBlocking(Integer neck, Integer rothead) { + moveHeadBlocking((double) neck, (double) rothead, null); + } + public void moveHeadBlocking(Double neck, Double rothead) { moveHeadBlocking(neck, rothead, null); } @@ -801,8 +934,10 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist); } - public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { - moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); + public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, + Integer wrist) { + moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, + (double) wrist); } public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { @@ -813,13 +948,18 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist); } - public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { - moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); + public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, + Integer wrist) { + moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, + (double) wrist); } public void moveTorso(Double topStom, Double midStom, Double lowStom) { - // the "right" way - invoke("publishMoveTorso", topStom, midStom, lowStom); + HashMap map = new HashMap<>(); + Optional.ofNullable(topStom).ifPresent(value -> map.put("topStom", value)); + Optional.ofNullable(midStom).ifPresent(value -> map.put("midStom", value)); + Optional.ofNullable(lowStom).ifPresent(value -> map.put("lowStom", value)); + invoke("publishMoveTorso", map); } public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { @@ -827,10 +967,10 @@ public void moveTorsoBlocking(Double topStom, Double midStom, Double lowStom) { sendToPeer("torso", "moveToBlocking", topStom, midStom, lowStom); } - public PredicateEvent onChangePredicate(PredicateEvent event) { + public Event onChangePredicate(Event event) { log.error("onChangePredicate {}", event); if (event.name.equals("topic")) { - invoke("publishEvent", String.format("TOPIC CHANGED TO %s", event.value)); + systemEvent(String.format("TOPIC CHANGED TO %s", event.value)); } // depending on configuration .... // call python ? @@ -839,11 +979,17 @@ public PredicateEvent onChangePredicate(PredicateEvent event) { return event; } + public void onConfigFinished(String configName) { + log.info("onConfigFinished"); + configStarted = false; + invoke("publishBoot"); + } + /** * comes in from runtime which owns the config list * * @param configList - * list of configs + * list of configs */ public void onConfigList(List configList) { this.configList = configList; @@ -857,8 +1003,8 @@ public void onCreated(String fullname) { public void onFinishedConfig(String configName) { log.info("onFinishedConfig"); - // invoke("publishEvent", "configFinished"); - invoke("publishFinishedConfig", configName); + // systemEvent("configFinished"); + invoke("publishConfigFinished", configName); } public void onGestureStatus(Status status) { @@ -870,6 +1016,170 @@ public void onGestureStatus(Status status) { unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus"); } + protected long heartbeatCount = 0; + + /** + * Allows or prevents sensor input from processing. + * Initial boot prevents sensor data from interfering with booting. + */ + protected boolean allowSensorInput = false; + + /** + * A generalized recurring event which can perform checks and various other + * methods or tasks. Heartbeats will not start until after boot stage. + */ + public void onHeartbeat(String name) { + try { + heartbeatCount++; + // heartbeats can start before config is + // done processing - so the following should + // not be dependent on config + Runtime runtime = Runtime.getInstance(); + + // no longer having problems with synchronization of other + // services - boot is immutable - no scripts can run until it is done + // and it will run InMoov2.py + if (fsm == null || "boot".equals(getState())) { + // need to keep trying to boot + boot(); + log.warn("not ready to leave boot - runtime still processing config"); + return; + } + // prepare report + if (!hasBooted) { + log.info("boot hasn't completed, will not process heartbeat"); + return; + } + + Long lastActivityTime = getLastActivityTime(); + + // FIXME lastActivityTime != 0 is bogus - the value should be null if + // never set + if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0 + && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) { + stateLastIdleTime = lastActivityTime; + } + + // will fire an idle event if there has been no activity + if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) { + fire("idle"); + stateLastIdleTime = System.currentTimeMillis(); + } + + // interval event firing + if (config.stateRandomInterval != null + && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) { + // fire("random"); + stateLastRandomTime = System.currentTimeMillis(); + } + + // FIXME publishInactivit(long time) + // FIXME publishLastActivity + + if (config.flashOnPir) { + Pir pir = (Pir) getPeer("pir"); + if (pir != null && pir.isActive()) { + flash("pir"); + } + } + + } catch (Exception e) { + error(e); + } + + // if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) { + // flash("pir"); + // } + + if (config.batteryInSystem) { + double batteryLevel = Runtime.getBatteryLevel(); + invoke("publishBatteryLevel", batteryLevel); + // FIXME - thresholding should always have old value or state + // so we don't pump endless errors + if (batteryLevel < 5) { + error("battery level < 5 percent"); + // systemEvent(BATTERY ERROR) + } else if (batteryLevel < 10) { + warn("battery level < 10 percent"); + // systemEvent(BATTERY WARN) + } + } + + // flash error until errors are cleared + if (config.flashOnErrors) { + if (errors.size() > 0) { + invoke("publishFlash", "error"); + } else { + // invoke("publishFlash", "heartbeat"); + } + } + + // has processed system events + // if led report ... + if (isPeerStarted("neoPixel") && !isSpeaking) { + // FIXME - publishLedMatrix + // FIXME direct type is bad :( + NeoPixel neo = (NeoPixel) getPeer("neoPixel"); + if (neo != null) { + // neo.clearPixelSet(); + + String state = getState(); + if (state != null) { + // log.info("hashcode {} {}", state, state.hashCode()); + // base bottom blue square + String color = CodecUtils.hashcodeToHex(state.hashCode()); + log.info("hashcode {} {} {}", state, color, state.hashCode()); + neo.setPixel(134, color); + neo.setPixel(135, color); + neo.setPixel(136, color); + neo.setPixel(137, color); + } + + if (heartbeatCount % 2 == 0) { + // top heartbeat square off + neo.setPixel(132, 0, 0, 0); + neo.setPixel(133, 0, 0, 0); + neo.setPixel(138, 0, 0, 0); + neo.setPixel(139, 0, 0, 0); + } else { + // top heartbeat square on + neo.setPixel(132, 12, 180, 212); + neo.setPixel(133, 12, 180, 212); + neo.setPixel(138, 12, 180, 212); + neo.setPixel(139, 12, 180, 212); + } + + // FIXME anywhere exposing type is bad + Pir pir = (Pir) getPeer("pir"); + if (pir != null && pir.isActive()) { + neo.setPixel(130, 225, 254, 0); + neo.setPixel(131, 225, 254, 0); + neo.setPixel(140, 225, 254, 0); + neo.setPixel(141, 225, 254, 0); + } else { + neo.setPixel(130, 0, 0, 0); + neo.setPixel(131, 0, 0, 0); + neo.setPixel(140, 0, 0, 0); + neo.setPixel(141, 0, 0, 0); + + } + neo.writeMatrix(); + } + } + + } + + public void onInactivity() { + log.info("onInactivity"); + + // powerDown ? + + } + + /** + * Central hub of input motion control. Potentially, all input from joysticks, + * quest2 controllers and headset, or any IK service could be sent here + */ @Override public void onJointAngles(Map angleMap) { log.debug("onJointAngles {}", angleMap); @@ -887,7 +1197,65 @@ public void onJointAngles(Map angleMap) { public void onJoystickInput(JoystickData input) throws Exception { // TODO timer ? to test and not send an event // switches to manual control ? - invoke("publishEvent", "joystick"); + systemEvent("joystick"); + } + + /** + * Centralized logging system will have all logging from all services, + * including lower level logs that do not propegate as statuses + * + * @param log + * - flushed log from Log service + */ + public void onLogEvents(List log) { + // scan for warn or errors + for (LogEntry entry : log) { + if ("ERROR".equals(entry.level) && errors.size() < 100) { + errors.add(entry); + } + } + } + + public void onMoveHead(Map map) { + InMoov2Head head = (InMoov2Head) getPeer("head"); + if (head != null) { + head.onMove(map); + } + } + + public void onMoveLeftArm(Map map) { + InMoov2Arm leftArm = (InMoov2Arm) getPeer("leftArm"); + if (leftArm != null) { + leftArm.onMove(map); + } + } + + public void onMoveLeftHand(Map map) { + InMoov2Hand leftHand = (InMoov2Hand) getPeer("leftHand"); + if (leftHand != null) { + leftHand.onMove(map); + } + } + + public void onMoveRightArm(Map map) { + InMoov2Arm rightArm = (InMoov2Arm) getPeer("rightArm"); + if (rightArm != null) { + rightArm.onMove(map); + } + } + + public void onMoveRightHand(Map map) { + InMoov2Hand rightHand = (InMoov2Hand) getPeer("rightHand"); + if (rightHand != null) { + rightHand.onMove(map); + } + } + + public void onMoveTorso(Map map) { + InMoov2Torso torso = (InMoov2Torso) getPeer("torso"); + if (torso != null) { + torso.onMove(map); + } } public String onNewState(String state) { @@ -911,16 +1279,16 @@ public OpenCVData onOpenCVData(OpenCVData data) { // FIXME - publish event with or without data ? String file reference return data; } - + /** * onPeak volume callback TODO - maybe make it variable with volume ? * * @param volume */ public void onPeak(double volume) { - if (config.neoPixelFlashWhenSpeaking && !configStarted) { + if (config.neoPixelFlashWhenSpeaking && !"boot".equals(getState())) { if (volume > 0.5) { - invoke("publishSpeakingFlash", "speaking"); + invoke("publishSpeakingFlash", "speaking"); } } } @@ -930,14 +1298,25 @@ public void onPeak(double volume) { * onPirOn flash neopixel */ public void onPirOn() { - // FIXME flash on config.flashOnBoot - invoke("publishFlash", "pir"); - ProgramAB chatBot = (ProgramAB)getPeer("chatBot"); - if (chatBot != null) { - String botState = chatBot.getPredicate("botState"); - if ("sleeping".equals(botState)) { - invoke("publishEvent", "WAKE"); - } + isPirOn = true; + // one advantage of re-publishing from inmoov - getName parameter can be + // added + if (allowSensorInput && !"boot".equals(getState())) { + SensorData pir = new SensorData(getName(), "Pir", isPirOn); + invoke("publishSensorData", pir); + } + } + + /** + * Pir off callback - FIXME NEEDS WORK + */ + public void onPirOff() { + isPirOn = false; + // one advantage of re-publishing from inmoov - getName parameter can be + // added + if (allowSensorInput && !"boot".equals(getState())) { + SensorData pir = new SensorData(getName(), "Pir", isPirOn); + invoke("publishSensorData", pir); } } @@ -972,9 +1351,9 @@ public boolean onSense(boolean b) { // setEvent("pir-sense-on" .. also sets it in config ? // config.handledEvents["pir-sense-on"] if (b) { - invoke("publishEvent", "PIR ON"); + systemEvent("PIR ON"); } else { - invoke("publishEvent", "PIR OFF"); + systemEvent("PIR OFF"); } return b; } @@ -986,7 +1365,7 @@ public boolean onSense(boolean b) { */ public void onStartConfig(String configName) { log.info("onStartConfig"); - invoke("publishStartConfig", configName); + invoke("publishConfigStarted", configName); } /** @@ -998,8 +1377,6 @@ public void onStartConfig(String configName) { */ @Override public void onStarted(String name) { - InMoov2Config c = (InMoov2Config) config; - log.info("onStarted {}", name); try { @@ -1008,7 +1385,7 @@ public void onStarted(String name) { // BAD IDEA - better to ask for a system report or an error report // if (runtime.isProcessingConfig()) { - // invoke("publishEvent", "CONFIG STARTED"); + // systemEvent("CONFIG STARTED"); // } String peerKey = getPeerKey(name); @@ -1018,11 +1395,11 @@ public void onStarted(String name) { } if (runtime.isProcessingConfig() && !configStarted) { - invoke("publishEvent", "CONFIG STARTED " + runtime.getConfigName()); + systemEvent("CONFIG STARTED " + runtime.getConfigName()); configStarted = true; } - invoke("publishEvent", "STARTED " + peerKey); + systemEvent("STARTED " + peerKey); switch (peerKey) { case "audioPlayer": @@ -1127,6 +1504,7 @@ public void onStarted(String name) { @Override public void onStopped(String name) { + log.info("service {} has stopped"); // using release peer for peer releasing // FIXME - auto remove subscriptions of peers? } @@ -1143,6 +1521,9 @@ public void onText(String text) { // TODO FIX/CHECK this, migrate from python land public void powerDown() { + // publishFlash(maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds, + // maxInactivityTimeSeconds, maxInactivityTimeSeconds) rest(); purgeTasks(); @@ -1185,15 +1566,210 @@ public void publish(String name, String method, Object... data) { invoke("publishMessage", msg); } - public String publishStartConfig(String configName) { + public String publishConfigStarted(String configName) { info("config %s started", configName); - invoke("publishEvent", "CONFIG STARTED " + configName); + systemEvent("CONFIG STARTED " + configName); return configName; } - public String publishFinishedConfig(String configName) { + public double publishBatteryLevel(double d) { + return d; + } + + /** + * At boot all services specified through configuration have started, or if no + * configuration has started minimally the InMoov2 service has started. During + * the processing of config and starting other services data will have + * accumulated, and at boot, some of data may now be inspected and processed + * in a synchronous single threaded way. With reporting after startup, vs + * during, other peer services are not needed (e.g. audioPlayer is no longer + * needed to be started "before" InMoov2 because when boot is called + * everything that is wanted has been started. + * + */ + public void boot() { + try { + // if ! ready + // FIXME don't overwrite if exists + Runtime runtime = Runtime.getInstance(); + runtime.saveDefault("InMoovDefault", getName(), "InMoov2", true); + + bootCount++; + log.info("boot count {}", bootCount); + + // thinking you shouldn't "boot" twice ? + if (hasBooted) { + log.warn("will not boot again"); + return; + } + + if (runtime.isProcessingConfig()) { + log.warn("runtime still processing config set {}, waiting ....", runtime.getConfigName()); + return; + } + + ServiceInterface python = Runtime.getService("python"); + if (python == null || !python.isReady()) { + log.warn("python not ready, waiting ...."); + return; + } + + // TODO - MAKE BOOT REPORT !!!! deliver it on a heartbeat + + // get service start and release life cycle events + // FIXME reduce to onStart + runtime.attachServiceLifeCycleListener(getName()); + + List services = Runtime.getServices(); + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // get events of new services and shutdown + subscribe("runtime", "shutdown"); + // power up loopback subscription + addListener(getName(), "powerUp"); + + subscribe("runtime", "publishConfigList"); + if (runtime.isProcessingConfig()) { + systemEvent("configStarted"); + } + + // subscribe to config processing events + // runtime callbacks publish the same a local + // subscribe("runtime", "publishConfigStarted", "publishConfigStarted"); + subscribe("runtime", "publishConfigFinished", "publishConfigFinished"); + + // chatbot getresponse attached to publishEvent + addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + + // copy config if it doesn't already exist + String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + List files = FileIO.getFileList(resourceBotDir); + for (File f : files) { + String botDir = "data/config/" + f.getName(); + File bDir = new File(botDir); + if (bDir.exists() || !f.isDirectory()) { + log.info("skipping data/config/{}", botDir); + } else { + log.info("will copy new data/config/{}", botDir); + try { + FileIO.copy(f.getAbsolutePath(), botDir); + } catch (Exception e) { + error(e); + } + } + } + runtime.invoke("publishConfigList"); + // FIXME - reduce the number of these + if (config.loadAppsScripts) { + loadAppsScripts(); + } + + if (config.loadInitScripts) { + loadInitScripts(); + } + + if (config.loadGestures) { + loadGestures(); + } + + execScript("InMoov2.py"); + log.info("here"); + + for (ServiceInterface si : services) { + if ("Servo".equals(si.getSimpleName())) { + send(si.getFullName(), "setAutoDisable", true); + } + } + + // FIXME - any interesting state changes or config stuff + // needs to be reported (once?) in onHeartbeat + + // FIXME - standardize multi-config examples should be available + // moved from startService to allow more simple control + // FIXME standard FileIO copyIfNotExists(src, dst) + // try { + // // copy config if it doesn't already exist + // String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); + // List files = FileIO.getFileList(resourceBotDir); + // for (File f : files) { + // String botDir = "data/config/" + f.getName(); + // File bDir = new File(botDir); + // if (bDir.exists() || !f.isDirectory()) { + // log.info("skipping data/config/{}", botDir); + // } else { + // log.info("will copy new data/config/{}", botDir); + // try { + // FileIO.copy(f.getAbsolutePath(), botDir); + // } catch (Exception e) { + // error(e); + // } + // } + // } + // } catch (Exception e) { + // error(e); + // } + + // FIXME - find good way of running an animation "through" a state + if (config.neoPixelBootGreen && getPeer("neoPixel") != null) { + NeoPixel neoPixel = (NeoPixel) getPeer("neoPixel"); + if (neoPixel != null) { + // invoke("publishPlayAnimation", config.bootAnimation); + } + } + + if (config.startupSound && getPeer("audioPlayer") != null) { + ((AudioFile) getPeer("audioPlayer")) + .playBlocking(FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3")); + } + + if (config.systemEventsOnBoot) { + // reporting on all services and config started + if (bootedConfig != null) { + // configuration was processed before booting + systemEvent("CONFIG LOADED %s", bootedConfig); + } + } + + // FIXME - important to do invoke & fsm needs to be consistent order + + // if speaking then turn off animation + + // publish all the errors + + // switch off animations + + // start heartbeat + // say starting heartbeat + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + + // say finished booting + fire("start"); + + // if (getPeer("mouth") != null) { + // AbstractSpeechSynthesis mouth = + // (AbstractSpeechSynthesis)getPeer("mouth"); + // mouth.setMute(wasMute); + // } + } catch (Exception e) { + hasBooted = false; + error(e); + } + + hasBooted = true; + + } + + public String publishConfigFinished(String configName) { info("config %s finished", configName); - invoke("publishEvent", "CONFIG LOADED " + configName); + systemEvent("CONFIG LOADED " + configName); return configName; } @@ -1208,6 +1784,28 @@ public List publishConfigList() { return configList; } + /** + * publishes a name of an animation, off/on control will be done through + * AudioListener interface + * + * @param name + * @return + */ + public String publishAnimation(String name) { + return name; + } + + /** + * publishes a name for NeoPixel.onFlash to consume, in a seperate channel to + * potentially be used by "speaking only" leds + * + * @param name + * @return + */ + public String publishSpeakingFlash(String name) { + return name; + } + /** * event publisher for the fsm - although other services potentially can * consume and filter this event channel @@ -1220,119 +1818,137 @@ public String publishEvent(String event) { } /** - * used to configure a flashing event - could use configuration to signal - * different colors and states + * publishes a name for NeoPixel.onFlash to consume * + * @param botType * @return */ public String publishFlash(String flashName) { return flashName; } + /** + * FSM is regularly driven by the heartbeat + * + * @return + */ public String publishHeartbeat() { - invoke("publishFlash", "heartbeat"); return getName(); } /** - * A more extensible interface point than publishEvent FIXME - create - * interface for this + *
    +   * 
    +   * Typically rebroadcast message from ProgramAB mrljson in aiml
        * 
    +   * The oob syntax is:
    +   *  <oob>
    +   *    <mrljson>
    +   *        [{method:on_new_user, data:[{"name":"<star/>"}]}]
    +   *    </mrljson>
    +   * </oob>
    +   * 
    +   * 
    * @param msg * @return */ public Message publishMessage(Message msg) { + msg.sender = getName(); return msg; } - public HashMap publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); - if ("left".equals(which)) { - invoke("publishMoveLeftArm", bicep, rotate, shoulder, omoplate); - } else { - invoke("publishMoveRightArm", bicep, rotate, shoulder, omoplate); - } + public Map publishMoveHead(Map map) { return map; } - public HashMap publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("which", which); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - if ("left".equals(which)) { - invoke("publishMoveLeftHand", thumb, index, majeure, ringFinger, pinky, wrist); - } else { - invoke("publishMoveRightHand", thumb, index, majeure, ringFinger, pinky, wrist); - } + public Map publishMoveLeftArm(Map map) { return map; } - public HashMap publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) { - HashMap map = new HashMap<>(); - map.put("neck", neck); - map.put("rothead", rothead); - map.put("eyeX", eyeX); - map.put("eyeY", eyeY); - map.put("jaw", jaw); - map.put("rollNeck", rollNeck); + public Map publishMoveLeftHand(Map map) { return map; } - public HashMap publishMoveLeftArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveRightArm(Map map) { return map; } - public HashMap publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); + public Map publishMoveRightHand(Map map) { return map; } - public HashMap publishMoveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) { - HashMap map = new HashMap<>(); - map.put("bicep", bicep); - map.put("rotate", rotate); - map.put("shoulder", shoulder); - map.put("omoplate", omoplate); + public Map publishMoveTorso(Map map) { return map; } - public HashMap publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { - HashMap map = new HashMap<>(); - map.put("thumb", thumb); - map.put("index", index); - map.put("majeure", majeure); - map.put("ringFinger", ringFinger); - map.put("pinky", pinky); - map.put("wrist", wrist); - return map; + public String publishPlayAudioFile(String filename) { + return filename; } - public HashMap publishMoveTorso(Double topStom, Double midStom, Double lowStom) { - HashMap map = new HashMap<>(); - map.put("topStom", topStom); - map.put("midStom", midStom); - map.put("lowStom", lowStom); - return map; + public String publishPlayAnimation(String animation) { + return animation; + } + + /** + * stop animation event + */ + public void publishStopAnimation() { + } + + /** + * initial state - updated on any state change + */ + String state = "boot"; + + public class InMoov2State { + public long ts = System.currentTimeMillis(); + public String src; + public String state; + public String event; + } + + /** + * The integration between the FiniteStateMachine (fsm) and the InMoov2 + * service and potentially other services (Python, ProgramAB) happens here. + * + * After boot all state changes get published here. + * + * Some InMoov2 service methods will be called here for "default + * implemenation" of states. If a user doesn't want to have that default + * implementation, they can change it by changing the definition of the state + * machine, and have a new state which will call a Python inmoov2 library + * callback. Overriding, appending, or completely transforming the behavior is + * all easily accomplished by managing the fsm and python inmoov2 library + * callbacks. + * + * Python inmoov2 callbacks ProgramAB topic switching + * + * Depending on config: + * + * + * @param stateChange + * @return + */ + public InMoov2State publishStateChange(FiniteStateMachine.StateChange stateChange) { + log.info("publishStateChange {}", stateChange); + InMoov2State state = new InMoov2State(); + state.src = getName(); + state.state = stateChange.state; + state.event = stateChange.event; + return state; + } + + /** + * event publisher for the fsm - although other services potentially can + * consume and filter this event channel + * + * @param event + * @return + */ + public String publishSystemEvent(String event) { + // well, it turned out underscore was a goofy selection, as underscore in + // aiml is wildcard ... duh + return String.format("SYSTEM_EVENT %s", event); } /** @@ -1343,11 +1959,21 @@ public String publishText(String text) { return text; } + /** + * default this will come from idle after some configurable time period + */ + public void random() { + Random random = (Random) getPeer("random"); + if (random != null) { + random.enable(); + } + } + @Override public void releasePeer(String peerKey) { super.releasePeer(peerKey); if (peerKey != null) { - invoke("publishEvent", "STOPPED " + peerKey); + systemEvent("STOPPED " + peerKey); } } @@ -1405,7 +2031,8 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null); } - public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { + public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, + Double wrist) { InMoov2Hand hand = getHand(which); if (hand == null) { warn("%s hand not started", which); @@ -1415,12 +2042,14 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur } @Deprecated - public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) { + public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, + Double pinky) { setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null); } @Deprecated - public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { + public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, + Double wrist) { setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist); } @@ -1436,7 +2065,8 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null); } - public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) { + public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, + Double rollNeckSpeed) { sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed); } @@ -1460,7 +2090,8 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl } @Deprecated - public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) { + public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, + Double rollNeckSpeed) { setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed); } @@ -1472,12 +2103,15 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate); } - public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { + public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, + Double wrist) { setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist); } - public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { - setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); + public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, + Integer wrist) { + setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, + (double) wrist); } @Override @@ -1520,7 +2154,7 @@ public void setOpenCV(OpenCV opencv) { } public boolean setPirPlaySounds(boolean b) { - getTypedConfig().pirPlaySounds = b; + config.pirPlaySounds = b; return b; } @@ -1546,12 +2180,15 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate); } - public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) { + public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, + Double wrist) { setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist); } - public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) { - setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist); + public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, + Integer wrist) { + setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, + (double) wrist); } public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) { @@ -1575,11 +2212,6 @@ public void setVoice(String name) { } } - // ----------------------------------------------------------------------------- - // These are methods added that were in InMoov1 that we no longer had in - // InMoov2. - // From original InMoov1 so we don't loose the - public void sleeping() { log.error("sleeping"); } @@ -1589,7 +2221,7 @@ public void speak(String toSpeak) { } public void speakAlert(String toSpeak) { - invoke("publishEvent", "ALERT"); + systemEvent("ALERT"); speakBlocking(toSpeak); } @@ -1626,91 +2258,7 @@ public void speakBlocking(String format, Object... args) { } } - public void startAll() throws Exception { - startAll(null, null); - } - - public void startAll(String leftPort, String rightPort) throws Exception { - startMouth(); - startChatBot(); - - // startHeadTracking(); - // startEyesTracking(); - // startOpenCV(); - startEar(); - - startServos(); - // startMouthControl(head.jaw, mouth); - - speakBlocking(get("STARTINGSEQUENCE")); - } - - public void startBrain() { - startChatBot(); - } - - public ProgramAB startChatBot() { - - try { - chatBot = (ProgramAB) startPeer("chatBot"); - - if (locale != null) { - chatBot.setCurrentBotName(locale.getTag()); - } - - // FIXME remove get en.properties stuff - speakBlocking(get("CHATBOTACTIVATED")); - - chatBot.attachTextPublisher(ear); - - // this.attach(chatBot); FIXME - attach as a TextPublisher - then - // re-publish - // FIXME - deal with language - // speakBlocking(get("CHATBOTACTIVATED")); - chatBot.repetitionCount(10); - // chatBot.setPath(getResourceDir() + fs + "chatbot"); - // chatBot.setPath(getDataDir() + "ProgramAB"); - chatBot.startSession("default", locale.getTag()); - // reset some parameters to default... - chatBot.setPredicate("topic", "default"); - chatBot.setPredicate("questionfirstinit", ""); - chatBot.setPredicate("tmpname", ""); - chatBot.setPredicate("null", ""); - // load last user session - if (!chatBot.getPredicate("name").isEmpty()) { - if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown") || chatBot.getPredicate("lastUsername").equals("default")) { - chatBot.setPredicate("lastUsername", chatBot.getPredicate("name")); - } - } - chatBot.setPredicate("parameterHowDoYouDo", ""); - chatBot.savePredicates(); - htmlFilter = (HtmlFilter) startPeer("htmlFilter");// Runtime.start("htmlFilter", - // "HtmlFilter"); - chatBot.attachTextListener(htmlFilter); - htmlFilter.attachTextListener((TextListener) getPeer("mouth")); - chatBot.attachTextListener(this); - // start session based on last recognized person - // if (!chatBot.getPredicate("default", "lastUsername").isEmpty() && - // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) { - // chatBot.startSession(chatBot.getPredicate("lastUsername")); - // } - if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown") - || chatBot.getPredicate("default", "firstinit").equals("started")) { - chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "FIRST INIT"); - } else { - chatBot.startSession(chatBot.getPredicate("default", "lastUsername")); - invoke("publishEvent", "WAKE UP"); - } - } catch (Exception e) { - speak("could not load chatBot"); - error(e.getMessage()); - speak(e.getMessage()); - } - broadcastState(); - return chatBot; - } - + @Deprecated /* this needs to be removed - runtime and config handle this */ public SpeechRecognizer startEar() { ear = (SpeechRecognizer) startPeer("ear"); @@ -1734,8 +2282,57 @@ public void startedGesture(String nameOfGesture) { } } + public class Heart implements Runnable { + private final ReentrantLock lock = new ReentrantLock(); + private Thread thread; + + @Override + public void run() { + if (lock.tryLock()) { + try { + while (!Thread.currentThread().isInterrupted()) { + invoke("publishHeartbeat"); + Thread.sleep(config.heartbeatInterval); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + log.info("heart stopping"); + thread = null; + } + } + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + config.heartbeat = false; + } else { + log.info("heart already stopped"); + } + } + + public void start() { + if (thread == null) { + log.info("starting heart"); + thread = new Thread(this, String.format("%s-heart", getName())); + thread.start(); + config.heartbeat = true; + } else { + log.info("heart already started"); + } + } + } + + private transient final Heart heart = new Heart(); + + protected boolean heartBeating = false; + + protected boolean isSpeaking = false; + public void startHeartbeat() { - addTask(1000, "publishHeartbeat"); + heart.start(); } // TODO - general objective "might" be to reduce peers down to something @@ -1796,59 +2393,19 @@ public ServiceInterface startPeer(String peer) { @Override public void startService() { - super.startService(); - - InMoov2Config c = (InMoov2Config) config; - Runtime runtime = Runtime.getInstance(); - - // get service start and release life cycle events - runtime.attachServiceLifeCycleListener(getName()); - - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); - } - } - // get events of new services and shutdown - subscribe("runtime", "shutdown"); - // power up loopback subscription - addListener(getName(), "powerUp"); + try { - subscribe("runtime", "publishConfigList"); - if (runtime.isProcessingConfig()) { - invoke("publishEvent", "configStarted"); - } - subscribe("runtime", "publishStartConfig"); - subscribe("runtime", "publishFinishedConfig"); + super.startService(); - // chatbot getresponse attached to publishEvent - addListener("publishEvent", getPeerName("chatBot"), "getResponse"); + // core part of InMoov + fsm = (FiniteStateMachine) startPeer("fsm"); + fsm.init(); - try { - // copy config if it doesn't already exist - String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config"); - List files = FileIO.getFileList(resourceBotDir); - for (File f : files) { - String botDir = "data/config/" + f.getName(); - File bDir = new File(botDir); - if (bDir.exists() || !f.isDirectory()) { - log.info("skipping data/config/{}", botDir); - } else { - log.info("will copy new data/config/{}", botDir); - try { - FileIO.copy(f.getAbsolutePath(), botDir); - } catch (Exception e) { - error(e); - } - } - } } catch (Exception e) { error(e); } - runtime.invoke("publishConfigList"); } public void startServos() { @@ -1876,12 +2433,13 @@ public void stop() { } public void stopGesture() { + // FIXME cannot be casting to Python Python p = (Python) Runtime.getService("python"); p.stop(); } public void stopHeartbeat() { - purgeTask("publishHeartbeat"); + heart.stop(); } public void stopNeopixelAnimation() { @@ -1908,7 +2466,21 @@ public void systemCheck() { Platform platform = Runtime.getPlatform(); setPredicate("system version", platform.getVersion()); // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + systemEvent("systemCheckFinished"); + } + + public String systemEvent(String format, Object... args) { + if (format == null) { + return null; + } + String event = null; + if (args == null) { + event = format; + } else { + event = String.format(format, args); + } + invoke("publishEvent", event); + return event; } // FIXME - if this is really desired it will drive local references for all @@ -1922,44 +2494,121 @@ public void waitTargetPos() { sendToPeer("leftArm", "waitTargetPos"); sendToPeer("torso", "waitTargetPos"); } - + public boolean setSpeechType(String speechType) { - + if (speechType == null) { error("cannot change speech type to null"); return false; } - + if (!speechType.contains(".")) { speechType = "org.myrobotlab.service." + speechType; } - + Runtime runtime = Runtime.getInstance(); String peerName = getName() + ".mouth"; Plan plan = runtime.getDefault(peerName, speechType); try { - SpeechSynthesisConfig mouth = (SpeechSynthesisConfig)plan.get(peerName); + SpeechSynthesisConfig mouth = (SpeechSynthesisConfig) plan.get(peerName); mouth.speechRecognizers = new String[] { getName() + ".ear" }; savePeerConfig("mouth", plan.get(peerName)); - + if (isPeerStarted("mouth")) { - // restart + // restart releasePeer("mouth"); startPeer("mouth"); } - - } catch(Exception e) { + + } catch (Exception e) { error("could not create config for %s", speechType); return false; } - + return true; - + // updatePeerType("mouth" /* getPeerName("mouth") */, speechType); // return speechType; } + public void closeRightHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveRightHand", map); + + } + + public void openRightHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveRightHand", map); + } + + public void closeLeftHand() { + + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 130.0); + map.put("index", 180.0); + map.put("majeure", 180.0); + map.put("ringFinger", 180.0); + map.put("pinky", 180.0); + invoke("publishMoveLeftHand", map); + + } + + public void openLeftHand() { + // if InMoov2Hand.close/open is used directly + // it prevents user's interception of the data + // and forces InMoov2Hand type to be used :( + // pub/sub is the way + + // hardcoded, but if necessary can be put in config + HashMap map = new HashMap<>(); + map.put("thumb", 0.0); + map.put("index", 0.0); + map.put("majeure", 0.0); + map.put("ringFinger", 0.0); + map.put("pinky", 0.0); + invoke("publishMoveLeftHand", map); + } + + public void openHands() { + openLeftHand(); + openRightHand(); + } + + public void closeHands() { + closeLeftHand(); + closeRightHand(); + } public static void main(String[] args) { try { @@ -1968,32 +2617,32 @@ public static void main(String[] args) { // Platform.setVirtual(true); // Runtime.start("s01", "Servo"); // Runtime.start("intro", "Intro"); - + Runtime runtime = Runtime.getInstance(); + runtime.saveDefault("default-i01", "i01", "InMoov2", true); Runtime.startConfig("dev"); - + + boolean done = true; + if (done) { + return; + } + WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui"); // webgui.setSsl(true); webgui.autoStartBrowser(false); // webgui.setPort(8888); webgui.startService(); - InMoov2 i01 = (InMoov2)Runtime.start("i01","InMoov2"); - - boolean done = true; - if (done) { - return; - } - + InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2"); - OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {}); + OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() { + }); ocvConfig.flip = true; i01.setPeerConfigValue("opencv", "flip", true); // i01.savePeerConfig("", null); - - // Runtime.startConfig("default"); - - // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" }); + // Runtime.startConfig("default"); + // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", + // "WebGui", "intro", "Intro", "python", "Python" }); Runtime.start("python", "Python"); // Runtime.start("ros", "Ros"); @@ -2031,11 +2680,15 @@ public static void main(String[] args) { random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); - random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, + 8.0, 25.0); + random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, + 8.0, 25.0); - random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); - random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0); + random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, + 130.0, 175.0); + random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, + 5.0, 40.0); random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); @@ -2055,5 +2708,35 @@ public static void main(String[] args) { } } + // FIXME - rebroadcast these + + @Override + public void onStartSpeaking(String utterance) { + isSpeaking = true; + } + + @Override + public void onEndSpeaking(String utterance) { + isSpeaking = false; + } + + /** + * Rebroadcasted topic change with source changed to this robot, + * python will consume it. + * + * @param topicChange + * @return the topic change + */ + public Event publishTopic(Event topicChange) { + topicChange.src = getName(); + return topicChange; + } + + // predicate change ? rebroadcasted ? + // FIXME if it went chatBot.publishSession -> InMoov2.onSession (if getState() != boot) InMoov2.publishSession + public Event publishSession(Event newSession) { + newSession.src = getName(); + return newSession; + } } diff --git a/src/main/java/org/myrobotlab/service/Lloyd.java b/src/main/java/org/myrobotlab/service/Lloyd.java index ca1dc561ff..4a5aeaf7b3 100755 --- a/src/main/java/org/myrobotlab/service/Lloyd.java +++ b/src/main/java/org/myrobotlab/service/Lloyd.java @@ -23,7 +23,7 @@ import org.myrobotlab.logging.LoggingFactory; import org.myrobotlab.opencv.OpenCVFilterDL4JTransfer; import org.myrobotlab.opencv.OpenCVFilterLloyd; -import org.myrobotlab.programab.OOBPayload; +import org.myrobotlab.programab.models.Oob; import org.myrobotlab.service.config.ServiceConfig; import org.myrobotlab.service.interfaces.SpeechRecognizer; import org.myrobotlab.service.interfaces.SpeechSynthesis; @@ -143,7 +143,7 @@ public void startMouth() { public void startBrain() throws IOException { brain = (ProgramAB) Runtime.start("brain", "ProgramAB"); // TODO: setup the AIML / chat bot directory for all of this. - brain.startSession("ProgramAB", "person", "lloyd"); + brain.setSession("person", "lloyd"); } public void initializeBrain() { @@ -240,13 +240,15 @@ public void tellMeAboutLookup() { } public void createKnowledgeLookup(String pattern, String fieldName, String prefix, String suffix) { - OOBPayload oobTag = createSolrFieldSearchOOB(fieldName); + Oob oobTag = createSolrFieldSearchOOB(fieldName); // TODO: handle (in the template) a zero hit result ?) - String template = prefix + OOBPayload.asBlockingOOBTag(oobTag) + suffix; - brain.addCategory(pattern, template); + + // FIXME +// String template = prefix + OOBPayload.asBlockingOOBTag(oobTag) + suffix; +// brain.addCategory(pattern, template); } - private OOBPayload createSolrFieldSearchOOB(String fieldName) { + private Oob createSolrFieldSearchOOB(String fieldName) { String serviceName = "cloudMemory"; // TODO: make this something that's completely abstracted out from here. String methodName = "fetchFirstResultSentence"; @@ -257,8 +259,10 @@ private OOBPayload createSolrFieldSearchOOB(String fieldName) { // +has_infobox:true"); params.add("infobox_name_ss: title: text: +" + fieldName + ":* +has_infobox:true"); params.add(fieldName); - OOBPayload oobTag = new OOBPayload(serviceName, methodName, params); - return oobTag; +// OOBPayload oobTag = new OOBPayload(serviceName, methodName, params); +// return oobTag; + // FIXME + return null; } public void startMemory() { diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java index af6c68c7bf..fbb433ac7a 100644 --- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java +++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java @@ -1,97 +1,186 @@ package org.myrobotlab.service.config; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import org.myrobotlab.framework.Plan; import org.myrobotlab.jme3.UserDataConfig; import org.myrobotlab.math.MapperLinear; +import org.myrobotlab.math.MapperSimple; import org.myrobotlab.service.Pid.PidData; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.config.FiniteStateMachineConfig.Transition; import org.myrobotlab.service.config.RandomConfig.RandomMessageConfig; +/** + * InMoov2Config - configuration for InMoov2 service + * - this is a "default" configuration + * If its configuration which will directly affect another service the naming + * pattern should be {peerName}{propertyName} + * e.g. neoPixelErrorRed + * + * FIXME make a color map that can be overridden + * + * @author GroG + * + */ public class InMoov2Config extends ServiceConfig { - public int analogPinFromSoundCard = 53; - - public int audioPollsBySeconds = 2; - - public boolean audioSignalProcessing=false; - + /** + * When the healthCheck is operating, it will check the battery level. + * If the battery level is < 5% it will publishFlash with red at regular interval + */ public boolean batteryInSystem = false; - - public boolean customSound=false; + + /** + * enable custom sound map for state changes + */ + public boolean customSound = false; + public boolean forceMicroOnIfSleeping = true; + + /** + * flashes if error has occurred - requires heartbeat + */ + public boolean healthCheckFlash = true; - public boolean healthCheckActivated = false; - - public int healthCheckTimerMs = 60000; - - public boolean heartbeat = false; - /** - * idle time measures the time the fsm is in an idle state + * flashes the neopixel every time a health check is preformed. + * green == good + * red == battery < 5% + */ + public boolean heartbeatFlash = false; + + /** + * Single heartbeat to drive InMoov2 .. it can check status, healthbeat, + * and fire events to the FSM. + * Checks battery level and sends a heartbeat flash on publishHeartbeat + * and onHeartbeat at a regular interval + */ + public boolean heartbeat = true; + + /** + * interval heath check processes in milliseconds */ - public boolean idleTimer = true; + public long heartbeatInterval = 3000; + /** + * loads all python gesture files in the gesture directory + */ public boolean loadGestures = true; + /** + * executes all scripts in the init directory on startup + */ + public boolean loadInitScripts = true; + /** * default to null - allow the OS to set it, unless explicilty set */ public String locale = null; // = "en-US"; - public boolean neoPixelBootGreen=true; + public boolean neoPixelBootGreen = true; public boolean neoPixelDownloadBlue = true; public boolean neoPixelErrorRed = true; - + public boolean neoPixelFlashWhenSpeaking = false; - - public boolean openCVFaceRecognizerActivated=true; - + + public boolean openCVFaceRecognizerActivated = true; + public boolean pirEnableTracking = false; - + + public boolean pirOnFlash = true; + /** - * play pir sounds when pir switching states - * sound located in data/InMoov2/sounds/pir-activated.mp3 - * sound located in data/InMoov2/sounds/pir-deactivated.mp3 + * play pir sounds when pir switching states sound located in + * data/InMoov2/sounds/pir-activated.mp3 sound located in + * data/InMoov2/sounds/pir-deactivated.mp3 */ public boolean pirPlaySounds = true; - + public boolean pirWakeUp = true; - + public boolean robotCanMoveHeadWhileSpeaking = true; - - + /** * startup and shutdown will pause inmoov - set the speed to this value then * attempt to move to rest */ public double shutdownStartupSpeed = 50; - + /** - * Sleep 5 minutes after last presence detected + * Sleep 5 minutes after last presence detected */ - public int sleepTimeoutMs=300000; - + public int sleepTimeoutMs = 300000; + public boolean startupSound = true; - public int trackingTimeoutMs=10000; + /** + * + */ + public boolean stateChangeIsMute = true; + + /** + * Interval in seconds for a idle state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to idle. Heartbeat will fire the event. + */ + public Integer stateIdleInterval = 120; - public String unlockInsult = "forgive me"; + /** + * Interval in seconds for a random state event to fire off. + * If the fsm is in a state which will allow transitioning, the InMoov2 + * state will transition to random. Heartbeat will fire the event. + */ + public Integer stateRandomInterval = 120; + + /** + * Determines if InMoov2 publish system events during boot state + */ + public boolean systemEventsOnBoot = false; + + /** + * The user id .. initially it would be the person starting the program + * but in a more general sense it would be the person "under focus" + * So, a newly discovered user may be the "user" - Python event hanlder + * on_new_user will set this when discovered + */ + public String user = null; + + /** + * Publish system event when state changes + */ + public boolean systemEventStateChange = true; + + public int trackingTimeoutMs = 10000; + + public String unlockInsult = "forgive me"; + public boolean virtual = false; + public String bootAnimation = "Theater Chase"; + + public boolean flashOnErrors = true; + + public boolean flashOnPir; + + public boolean loadAppsScripts = true; + public InMoov2Config() { } @Override public Plan getDefault(Plan plan, String name) { super.getDefault(plan, name); + + // FIXME define global peers named "python" "webgui" etc... + // peers FIXME global opencv addDefaultPeerConfig(plan, name, "audioPlayer", "AudioFile", true); @@ -101,6 +190,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "ear", "WebkitSpeechRecognition", false); addDefaultPeerConfig(plan, name, "eyeTracking", "Tracking", false); addDefaultPeerConfig(plan, name, "fsm", "FiniteStateMachine", false); + addDefaultPeerConfig(plan, name, "log", "Log", false); addDefaultPeerConfig(plan, name, "gpt3", "Gpt3", false); addDefaultPeerConfig(plan, name, "head", "InMoov2Head", false); addDefaultPeerConfig(plan, name, "headTracking", "Tracking", false); @@ -111,6 +201,7 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "leftArm", "InMoov2Arm", false); addDefaultPeerConfig(plan, name, "leftHand", "InMoov2Hand", false); addDefaultPeerConfig(plan, name, "mouth", "MarySpeech", false); + addDefaultPeerConfig(plan, name, "mouth.audioFile", "AudioFile", false); addDefaultPeerConfig(plan, name, "mouthControl", "MouthControl", false); addDefaultPeerConfig(plan, name, "neoPixel", "NeoPixel", false); addDefaultPeerConfig(plan, name, "opencv", "OpenCV", false); @@ -127,6 +218,25 @@ public Plan getDefault(Plan plan, String name) { addDefaultPeerConfig(plan, name, "torso", "InMoov2Torso", false); addDefaultPeerConfig(plan, name, "ultrasonicRight", "UltrasonicSensor", false); addDefaultPeerConfig(plan, name, "ultrasonicLeft", "UltrasonicSensor", false); + addDefaultPeerConfig(plan, name, "vertx", "Vertx", false); + addDefaultPeerConfig(plan, name, "webxr", "WebXR", false); + + WebXRConfig webxr = (WebXRConfig) plan.get(getPeerName("webxr")); + + Map map = new HashMap<>(); + MapperSimple mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.neck", mapper); + webxr.controllerMappings.put("head.orientation.pitch", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.rothead", mapper); + webxr.controllerMappings.put("head.orientation.yaw", map); + + map = new HashMap<>(); + mapper = new MapperSimple(-0.5, 0.5, 0, 180); + map.put("i01.head.roll", mapper); + webxr.controllerMappings.put("head.orientation.roll", map); MouthControlConfig mouthControl = (MouthControlConfig) plan.get(getPeerName("mouthControl")); @@ -141,6 +251,8 @@ public Plan getDefault(Plan plan, String name) { mouthControl.mouth = i01Name + ".mouth"; ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot")); + chatBot.listeners = new ArrayList<>(); + Runtime runtime = Runtime.getInstance(); String[] bots = new String[] { "cn-ZH", "en-US", "fi-FI", "hi-IN", "nl-NL", "ru-RU", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-PT", "tr-TR" }; String tag = runtime.getLocaleTag(); @@ -149,19 +261,17 @@ public Plan getDefault(Plan plan, String name) { String lang = tagparts[0]; for (String b : bots) { if (b.startsWith(lang)) { - chatBot.currentBotName = b; + chatBot.botType = b; break; } } } - - chatBot.currentUserName = "human"; - - // chatBot.textListeners = new String[] { name + ".htmlFilter" }; - if (chatBot.listeners == null) { - chatBot.listeners = new ArrayList<>(); - } - chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + + // chatBot.currentUserName = "human"; is already default + + Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3")); + gpt3.listeners = new ArrayList<>(); + gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); HtmlFilterConfig htmlFilter = (HtmlFilterConfig) plan.get(getPeerName("htmlFilter")); // htmlFilter.textListeners = new String[] { name + ".mouth" }; @@ -176,21 +286,15 @@ public Plan getDefault(Plan plan, String name) { mouth.voice = "Mark"; mouth.speechRecognizers = new String[] { name + ".ear" }; - // == Peer - servoMixer ============================= - // setup name references to different services - ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer")); - servoMixer.listeners = new ArrayList<>(); - servoMixer.listeners.add(new Listener("publishText", name + ".mouth", "onText")); - //servoMixer.listeners.add(new Listener("publishText", name + ".chatBot", "onText")); // == Peer - ear ============================= // setup name references to different services WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear")); - ear.listeners = new ArrayList<>(); + ear.listeners = new ArrayList<>(); ear.listeners.add(new Listener("publishText", name + ".chatBot", "onText")); ear.listening = true; // remove, should only need ServiceConfig.listeners - ear.textListeners = new String[]{name + ".chatBot"}; + ear.textListeners = new String[] { name + ".chatBot" }; JMonkeyEngineConfig simulator = (JMonkeyEngineConfig) plan.get(getPeerName("simulator")); @@ -259,63 +363,72 @@ public Plan getDefault(Plan plan, String name) { simulator.cameraLookAt = name + ".torso.lowStom"; FiniteStateMachineConfig fsm = (FiniteStateMachineConfig) plan.get(getPeerName("fsm")); - // TODO - events easily gotten from InMoov data ?? auto callbacks in python if exists ? + // TODO - events easily gotten from InMoov data ?? auto callbacks in python + // if exists ? + fsm.listeners = new ArrayList<>(); fsm.current = "boot"; - fsm.transitions.add(new Transition("boot", "configStarted", "applyingConfig")); - fsm.transitions.add(new Transition("applyingConfig", "getUserInfo", "getUserInfo")); - fsm.transitions.add(new Transition("applyingConfig", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("applyingConfig", "wake", "awake")); - fsm.transitions.add(new Transition("getUserInfo", "systemCheck", "systemCheck")); - fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", "awake")); - fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); + fsm.transitions.add(new Transition("boot", "start", "start")); + // fsm.transitions.add(new Transition("start", "idle", "idle")); + fsm.transitions.add(new Transition("start", "first_init", "first_init")); + fsm.transitions.add(new Transition("start", "wake", "wake")); + fsm.transitions.add(new Transition("idle", "random", "random")); + fsm.transitions.add(new Transition("random", "idle", "idle")); + fsm.transitions.add(new Transition("idle", "sleep", "sleep")); + fsm.transitions.add(new Transition("sleep", "wake", "wake")); + fsm.transitions.add(new Transition("idle", "power_down", "power_down")); + fsm.transitions.add(new Transition("wake", "idle", "idle")); + // fsm.transitions.add(new Transition("wake", "first_init", "first_init")); + // powerDown to shutdown + // fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished", + // "awake")); + // fsm.transitions.add(new Transition("awake", "sleep", "sleeping")); - - PirConfig pir = (PirConfig) plan.get(getPeerName("pir")); - pir.pin = "23"; + pir.pin = "D23"; pir.controller = name + ".left"; pir.listeners = new ArrayList<>(); - pir.listeners.add(new Listener("publishPirOn", name, "onPirOn")); - + pir.listeners.add(new Listener("publishPirOn", name)); + pir.listeners.add(new Listener("publishPirOff", name)); + // == Peer - random ============================= RandomConfig random = (RandomConfig) plan.get(getPeerName("random")); random.enabled = false; // setup name references to different services - RandomMessageConfig rm = new RandomMessageConfig(name, "setLeftArmSpeed", 3000, 8000, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + RandomMessageConfig rm = new RandomMessageConfig(name, "setLeftArmSpeed", 3000, 8000, 8, 25, 8, 25, 8, 25, 8, 25); random.randomMessages.put(name + ".setLeftArmSpeed", rm); - rm = new RandomMessageConfig(name, "setRightArmSpeed", 3000, 8000, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + rm = new RandomMessageConfig(name, "setRightArmSpeed", 3000, 8000, 8, 25, 8, 25, 8, 25, 8, 25); random.randomMessages.put(name + ".setRightArmSpeed", rm); - rm = new RandomMessageConfig(name, "moveLeftArm", 000, 8000, 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + rm = new RandomMessageConfig(name, "moveLeftArm", 000, 8000, 0, 5, 85, 95, 25, 30, 10, 15); random.randomMessages.put(name + ".moveLeftArm", rm); - rm = new RandomMessageConfig(name, "moveRightArm", 3000, 8000, 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0); + rm = new RandomMessageConfig(name, "moveRightArm", 3000, 8000, 0, 5, 85, 95, 25, 30, 10, 15); random.randomMessages.put(name + ".moveRightArm", rm); - rm = new RandomMessageConfig(name, "setLeftHandSpeed", 3000, 8000, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + rm = new RandomMessageConfig(name, "setLeftHandSpeed", 3000, 8000, 8, 25, 8, 25, 8, 25, 8, 25, 8, 25, 8, 25); random.randomMessages.put(name + ".setLeftHandSpeed", rm); - rm = new RandomMessageConfig(name, "setRightHandSpeed", 3000, 8000, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0); + rm = new RandomMessageConfig(name, "setRightHandSpeed", 3000, 8000, 8, 25, 8, 25, 8, 25, 8, 25, 8, 25, 8, 25); random.randomMessages.put(name + ".setRightHandSpeed", rm); - rm = new RandomMessageConfig(name, "moveLeftHand", 3000, 8000, 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); + rm = new RandomMessageConfig(name, "moveLeftHand", 3000, 8000, 10, 160, 10, 60, 10, 60, 10, 60, 10, 60, 130, 175); random.randomMessages.put(name + ".moveLeftHand", rm); - rm = new RandomMessageConfig(name, "moveRightHand", 3000, 8000, 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0); + rm = new RandomMessageConfig(name, "moveRightHand", 3000, 8000, 10, 160, 10, 60, 10, 60, 10, 60, 10, 60, 130, 175); random.randomMessages.put(name + ".moveRightHand", rm); - rm = new RandomMessageConfig(name, "setHeadSpeed",3000, 8000, 8.0, 20.0, 8.0, 20.0, 8.0, 20.0); + rm = new RandomMessageConfig(name, "setHeadSpeed", 3000, 8000, 8, 20, 8, 20, 8, 20); random.randomMessages.put(name + ".setHeadSpeed", rm); - rm = new RandomMessageConfig(name, "moveHead", 3000, 8000, 70.0, 110.0, 65.0, 115.0, 70.0, 110.0); + rm = new RandomMessageConfig(name, "moveHead", 3000, 8000, 70, 110, 65, 115, 70, 110); random.randomMessages.put(name + ".moveHead", rm); - rm = new RandomMessageConfig(name , "setTorsoSpeed", 3000, 8000, 2.0, 5.0, 2.0, 5.0, 2.0, 5.0); + rm = new RandomMessageConfig(name, "setTorsoSpeed", 3000, 8000, 2, 5, 2, 5, 2, 5); random.randomMessages.put(name + ".setTorsoSpeed", rm); - rm = new RandomMessageConfig(name, "moveTorso", 3000, 8000, 85.0, 95.0, 88.0, 93.0, 70.0, 110.0); + rm = new RandomMessageConfig(name, "moveTorso", 3000, 8000, 85, 95, 88, 93, 70, 110); random.randomMessages.put(name + ".moveTorso", rm); // == Peer - headTracking ============================= @@ -387,17 +500,114 @@ public Plan getDefault(Plan plan, String name) { plan.remove(name + ".eyeTracking.controller"); plan.remove(name + ".eyeTracking.controller.serial"); plan.remove(name + ".eyeTracking.cv"); - + + // LOOPBACK PUBLISHING - ITS A GREAT WAY TO SUPPORT + // EXTENSIBLE AND OVERRIDABLE BEHAVIORS + // inmoov2 default listeners listeners = new ArrayList<>(); // FIXME - should be getPeerName("neoPixel") - listeners.add(new Listener("publishFlash", name + ".neoPixel", "onLedDisplay")); - listeners.add(new Listener("publishEvent", name + ".fsm")); + // loopbacks allow user to override or extend with python +// listeners.add(new Listener("publishBoot", name)); + listeners.add(new Listener("publishHeartbeat", name)); +// listeners.add(new Listener("publishConfigFinished", name)); +// listeners.add(new Listener("publishStateChange", name)); + + // listeners.add(new Listener("publishPowerUp", name)); + // listeners.add(new Listener("publishPowerDown", name)); + // listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + LogConfig log = (LogConfig) plan.get(getPeerName("log")); + log.listeners = new ArrayList<>(); + log.listeners.add(new Listener("publishLogEvents", name)); + + // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); + // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + + // InMoov2 --to--> service + listeners.add(new Listener("publishFlash", getPeerName("neoPixel"))); + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + + listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel"))); + listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel"))); + + // listeners.add(new Listener("publishPowerUp", name)); + // listeners.add(new Listener("publishPowerDown", name)); + // listeners.add(new Listener("publishError", name)); + + listeners.add(new Listener("publishMoveHead", name)); + listeners.add(new Listener("publishMoveRightArm", name)); + listeners.add(new Listener("publishMoveLeftArm", name)); + listeners.add(new Listener("publishMoveRightHand", name)); + listeners.add(new Listener("publishMoveLeftHand", name)); + listeners.add(new Listener("publishMoveTorso", name)); + + + // service --to--> InMoov2 + AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile")); + mouth_audioFile.listeners = new ArrayList<>(); + mouth_audioFile.listeners.add(new Listener("publishPeak", name)); + + // service --> InMoov2 FIXME !!! - rebroadcast with event and event.src + mouth.listeners = new ArrayList<>(); + mouth.listeners.add(new Listener("publishStartSpeaking", name)); + mouth.listeners.add(new Listener("publishEndSpeaking", name)); + + // service --> hardcoded service name - NOT GOOD :( + mouth.listeners.add(new Listener("publishStartSpeaking", "python")); + mouth.listeners.add(new Listener("publishEndSpeaking", "python")); + + // mouth --> ear .. should it be audio file instead "closer to the actual audio"? + mouth.listeners.add(new Listener("publishStartSpeaking", getPeerName("ear"))); + mouth.listeners.add(new Listener("publishEndSpeaking", getPeerName("ear"))); + + + + webxr.listeners = new ArrayList<>(); + webxr.listeners.add(new Listener("publishJointAngles", name)); // TODO rebroadcast this + + // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name)); + // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name)); + + // InMoov2 --to--> service + listeners.add(new Listener("publishEvent", getPeerName("chatBot"), "getResponse")); + listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer"))); + listeners.add(new Listener("publishPython", "python")); + listeners.add(new Listener("publishSensorData", "python")); + listeners.add(new Listener("publishPredicate", "python")); + listeners.add(new Listener("publishStateChange", "python")); + listeners.add(new Listener("publishSession", "python")); + listeners.add(new Listener("publishMessage", "python")); + + + // service --to--> service + ServoMixerConfig servoMixer = (ServoMixerConfig) plan.get(getPeerName("servoMixer")); + servoMixer.listeners = new ArrayList<>(); + servoMixer.listeners.add(new Listener("publishText", getPeerName("mouth"), "onText")); + chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText")); + + + // service --> InMoov2 --> rebroadcast correctly with src + chatBot.listeners.add(new Listener("publishTopic", name, "publishTopic")); + chatBot.listeners.add(new Listener("publishPredicate", name, "publishPredicate")); + chatBot.listeners.add(new Listener("publishSession", name, "publishSession")); + chatBot.listeners.add(new Listener("publishMessage", name, "publishMessage")); + fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange")); + + // remove the auto-added starts in the plan's runtime RuntimConfig.registry plan.removeStartsWith(name + "."); - + // rtConfig.add(name); // <-- adding i01 / not needed return plan; From 768575de3db460ffa6c17f4f666885e88455feea Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:09:59 -0800 Subject: [PATCH 097/232] more --- .../generics/SlidingWindowList.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/org/myrobotlab/generics/SlidingWindowList.java diff --git a/src/main/java/org/myrobotlab/generics/SlidingWindowList.java b/src/main/java/org/myrobotlab/generics/SlidingWindowList.java new file mode 100644 index 0000000000..2c048a532f --- /dev/null +++ b/src/main/java/org/myrobotlab/generics/SlidingWindowList.java @@ -0,0 +1,22 @@ +package org.myrobotlab.generics; + +import java.util.ArrayList; + +public class SlidingWindowList extends ArrayList { + private static final long serialVersionUID = 1L; + private final int maxSize; + + public SlidingWindowList(int maxSize) { + this.maxSize = maxSize; + } + + @Override + public boolean add(E element) { + boolean added = super.add(element); + if (size() > maxSize) { + removeRange(0, size() - maxSize); // Remove oldest elements if size exceeds maxSize + } + return added; + } + +} From 0dce02c68786a53818de146bbf2ceb51ccc20e73 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:11:38 -0800 Subject: [PATCH 098/232] more --- .../java/org/myrobotlab/service/Shoutbox.java | 2 +- .../myrobotlab/service/data/SensorData.java | 42 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/Shoutbox.java b/src/main/java/org/myrobotlab/service/Shoutbox.java index a567ae073e..e063de93a1 100644 --- a/src/main/java/org/myrobotlab/service/Shoutbox.java +++ b/src/main/java/org/myrobotlab/service/Shoutbox.java @@ -453,7 +453,7 @@ public void startChatBot() throws IOException { return; } chatbot = (ProgramAB) Runtime.start("chatbot", "ProgramAB"); - chatbot.startSession("ProgramAB", "alice2"); + chatbot.setSession("ProgramAB", "alice2"); chatbot.addResponseListener(this); } diff --git a/src/main/java/org/myrobotlab/service/data/SensorData.java b/src/main/java/org/myrobotlab/service/data/SensorData.java index 363553b68a..7a922cebaa 100644 --- a/src/main/java/org/myrobotlab/service/data/SensorData.java +++ b/src/main/java/org/myrobotlab/service/data/SensorData.java @@ -2,17 +2,47 @@ import java.io.Serializable; +/** + * A generalized bucket for simplified channels to hold + * any type of sensor information and its source. + * + * @author GroG + * + */ public class SensorData implements Serializable { private static final long serialVersionUID = 1L; - Object data; + + /** + * timestamp + */ + public long ts = System.currentTimeMillis(); + + /** + * Type of string data so downstream receivers can + * interpret data field correctly, typically the + * name of the type of service which generated it. + */ + public String type; + + /** + * Service name where data came from. + */ + public String src; + + /** + * data of sensor + */ + public Object data; - public SensorData(Object data) { - this.data = data; + public SensorData() { } - - public Object getData() { - return data; + + public SensorData(String src, String type, Object data) { + this.src = src; + this.type = type; + this.data = data; } + } From 2f50853a23d938dc7c514e740aba7ba54ca8a0ca Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:14:28 -0800 Subject: [PATCH 099/232] hashToHex --- src/main/java/org/myrobotlab/codec/CodecUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java index ba528b7a10..eb20b2191b 100644 --- a/src/main/java/org/myrobotlab/codec/CodecUtils.java +++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java @@ -1685,4 +1685,10 @@ public static int[] hexToRGB(String hexValue) { } return rgb; } + + public static String hashcodeToHex(int hashCode) { + String hexString = Long.toHexString(hashCode).toUpperCase(); + return String.format("%6s", hexString).replace(' ', '0').substring(0, 6); + } + } From 10eb5e0707e34176b5bc665e76d4c8106a418652 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:36:20 -0800 Subject: [PATCH 100/232] state change definition --- .../service/FiniteStateMachine.java | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java index d04436f1e3..7a290bc459 100644 --- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java +++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java @@ -12,6 +12,7 @@ import org.myrobotlab.framework.Service; import org.myrobotlab.framework.interfaces.MessageListener; import org.myrobotlab.framework.interfaces.ServiceInterface; +import org.myrobotlab.generics.SlidingWindowList; import org.myrobotlab.logging.Level; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.logging.LoggingFactory; @@ -51,7 +52,7 @@ public class FiniteStateMachine extends Service { /** * state history of fsm */ - protected List history = new ArrayList<>(); + protected List history = new SlidingWindowList<>(100); // TODO - .from("A").to("B").on(Messages.ANY) // TODO - .from("A").to("B").on(Messages.EMPTY) @@ -65,17 +66,27 @@ public class Tuple { } public class StateChange { - public String last; - public String current; + /** + * timestamp + */ + public long ts = System.currentTimeMillis(); + + /** + * current new state + */ + public String state; + + /** + * event which activated new state + */ public String event; - public StateChange(String last, String current, String event) { - this.last = last; - this.current = current; + public StateChange(String current, String event) { + this.state = current; this.event = event; } public String toString() { - return String.format("%s --%s--> %s", last, event, current); + return String.format("%s --%s--> %s", last, event, state); } } @@ -113,11 +124,8 @@ 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()); + history.add(new StateChange(state.getName(), String.format("%s.setCurrent", getName()))); } } @@ -194,8 +202,9 @@ 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("publishStateChange", new StateChange(last.getName(), current.getName(), event)); - history.add(current.getName()); + StateChange stateChange = new StateChange(current.getName(), event); + invoke("publishStateChange", stateChange); + history.add(stateChange); } } catch (Exception e) { log.error("fire threw", e); @@ -247,7 +256,7 @@ public StateChange publishStateChange(StateChange 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(stateChange.current), null); + org.myrobotlab.framework.Message msg = org.myrobotlab.framework.Message.createMessage(getName(), listener, CodecUtils.getCallbackTopicName(stateChange.state), null); service.in(msg); } } @@ -419,7 +428,7 @@ public void setCurrent(String state) { stateMachine.setCurrent(state); current = stateMachine.getCurrent(); if (last != null && !last.equals(current)) { - invoke("publishStateChange", new StateChange(last.getName(), current.getName(), null)); + invoke("publishStateChange", new StateChange(current.getName(), String.format("%s.setCurrent", getName()))); } } catch (Exception e) { log.error("setCurrent threw", e); @@ -427,4 +436,12 @@ public void setCurrent(String state) { } } + public String getPreviousState() { + if (history.size() == 0) { + return null; + } else { + return history.get(history.size() - 2).state; + } + } + } \ No newline at end of file From 0473e76b152582757d5ee48f018b55e0492e75f3 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:45:05 -0800 Subject: [PATCH 101/232] no more oob payload --- .../myrobotlab/programab/OOBPayloadTest.java | 32 ------------------- .../org/myrobotlab/service/HarryTest.java | 6 ++-- 2 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 src/test/java/org/myrobotlab/programab/OOBPayloadTest.java diff --git a/src/test/java/org/myrobotlab/programab/OOBPayloadTest.java b/src/test/java/org/myrobotlab/programab/OOBPayloadTest.java deleted file mode 100644 index 3761bc5c29..0000000000 --- a/src/test/java/org/myrobotlab/programab/OOBPayloadTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.myrobotlab.programab; - -import static org.junit.Assert.assertEquals; - -import java.util.ArrayList; - -import org.junit.Test; -import org.myrobotlab.test.AbstractTest; - -public class OOBPayloadTest extends AbstractTest { - - @Test - public void basicTestOOBPayload() { - // test the getters/setters of oob object. - OOBPayload payload = new OOBPayload(); - payload.setMethodName("foo"); - ArrayList params = new ArrayList(); - payload.setParams(params); - payload.setServiceName("fooservice"); - - assertEquals(payload.getMethodName(), "foo"); - assertEquals(payload.getParams(), params); - assertEquals(payload.getServiceName(), "fooservice"); - - OOBPayload pl2 = new OOBPayload("boo2", "fooo2", params); - assertEquals(pl2.getMethodName(), "fooo2"); - assertEquals(pl2.getParams(), params); - assertEquals(pl2.getServiceName(), "boo2"); - - } - -} diff --git a/src/test/java/org/myrobotlab/service/HarryTest.java b/src/test/java/org/myrobotlab/service/HarryTest.java index 29527a0845..b635ea2a6b 100755 --- a/src/test/java/org/myrobotlab/service/HarryTest.java +++ b/src/test/java/org/myrobotlab/service/HarryTest.java @@ -144,7 +144,8 @@ public void testDynamic() throws SolrServerException, IOException, InterruptedEx solr.startEmbedded(); createAIML(); ProgramAB harry = (ProgramAB) Runtime.start("harry", "ProgramAB"); - harry.startSession("testbots", "username", "test"); + harry.setBotType("testbots"); + harry.startSession("username", "test", true); goLearnStuff(solr, harry); @@ -166,7 +167,8 @@ public void testHarry() throws Exception { solr.startEmbedded(); createAIML(); ProgramAB harry = (ProgramAB) Runtime.start("harry", "ProgramAB"); - harry.startSession("testbots", "username", "test"); + harry.setSession("username", "test"); + // harry.startSession("username", "test"); // start the opencv service with the yolo filter. OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV"); From a0e79dadfad6a6022dda5b4169c4173221466052 Mon Sep 17 00:00:00 2001 From: grog Date: Tue, 5 Dec 2023 11:50:08 -0800 Subject: [PATCH 102/232] formatted xml and fixed ui --- .../WebGui/app/service/js/ProgramABGui.js | 432 +++++++++--------- .../app/service/views/ProgramABGui.html | 12 +- .../service/ArduinoMotorPotTest.java | 2 +- .../ProgramAB/bots/lloyd/aiml/lloyd.aiml | 394 ++++++++++------ 4 files changed, 473 insertions(+), 367 deletions(-) diff --git a/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js b/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js index 38211728ff..0e34121c1c 100644 --- a/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/ProgramABGui.js @@ -1,6 +1,12 @@ -angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', ['$scope', '$compile', 'mrl', '$uibModal', '$sce', function($scope, $compile, mrl, $uibModal, $sce) { +angular.module("mrlapp.service.ProgramABGui", []).controller("ProgramABGuiCtrl", [ + "$scope", + "$compile", + "mrl", + "$uibModal", + "$sce", + function ($scope, $compile, mrl, $uibModal, $sce) { // $modal ???? - console.info('ProgramABGuiCtrl') + console.info("ProgramABGuiCtrl") // grab the self and message var _self = this var startDialog = null @@ -8,8 +14,8 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', // use $scope only when the variable // needs to interract with the display - $scope.currentUserName = '' - $scope.utterance = '' + $scope.currentUserName = "" + $scope.utterance = "" $scope.currentSessionKey = null $scope.status = null $scope.predicates = [] @@ -18,10 +24,10 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', $scope.aimlEditor = null $scope.tabs = { - "selected": 1 + selected: 1, } $scope.tabsRight = { - "selected": 1 + selected: 1, } // active tab index @@ -29,7 +35,7 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', $scope.aimlFile = "blah \n blah " $scope.aimlFileData = { - "data": "HELLO THERE !!!" + data: "HELLO THERE !!!", } $scope.lastResponse @@ -38,7 +44,7 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', // be put in an object to be effectively modified // when $sope is used $scope.edit = { - properties: false + properties: false, } $scope.chatLog = [] @@ -47,269 +53,263 @@ angular.module('mrlapp.service.ProgramABGui', []).controller('ProgramABGuiCtrl', $scope.log = [] // following the template. - this.updateState = function(service) { - // use another scope var to transfer/merge selection - // from user - service.currentSession is always read-only - // all service data should never be written to, only read from - $scope.currentUserName = service.config.currentUserName - $scope.service = service - $scope.currentSessionKey = $scope.getCurrentSessionKey() - - /* + this.updateState = function (service) { + // use another scope var to transfer/merge selection + // from user - service.currentSession is always read-only + // all service data should never be written to, only read from + $scope.currentUserName = service.config.username + $scope.service = service + $scope.currentSessionKey = $scope.getCurrentSessionKey() + + /* for (let bot in $scope.service.sessions){ for (let username in $scope.service.sessions[bot]){ console.info(username) } } */ - } - this.onMsg = function(inMsg) { - // console.info("ProgramABGui.onMsg(" + inMsg.method + ')') - let data = inMsg.data[0] - - switch (inMsg.method) { - - case 'onStatus': - $scope.status = data; - $scope.$apply() - break - - case 'onBotImage': - $scope.currentBotImage = data - $scope.$apply() - break - - case 'onState': - _self.updateState(data) - $scope.$apply() - break - - case 'onTopic': - $scope.service.currentTopic = data - $scope.$apply() - break - - case 'onAimlFile': - $scope.aimlFileData.data = data - $scope.$apply() - break - - case 'onPredicates': - $scope.predicates = data - $scope.$apply() - break - - case 'onPredicate': - $scope.predicates[data.name] = data.value - $scope.$apply() - break - - - case 'onRequest': - var textData = data - $scope.chatLog.unshift({ - type: 'User', - name: $scope.currentUserName, - text: $sce.trustAsHtml(textData) - }) - console.info('onRequest', textData) - $scope.$apply() - break - case 'onResponse': - var textData = data - $scope.chatLog.unshift({ - type: 'Bot', - name: $scope.service.config.currentBotName, - text: $sce.trustAsHtml(data.msg) - }) - $scope.lastResponse = textData - $scope.$apply() - break - case 'onLog': - var textData = data - let filename = null - parts = textData.split(" ") - if (parts.length > 2 && parts[1] == "Matched:") { - filename = parts[parts.length - 1] - textData = textData.substr(0, textData.lastIndexOf(' ') + 1) - // pos0 = textData.lastIndexOf(' ') + 1 - // url = textData.substr(0,pos0) + '' - // url = textData.substr(0,pos0) + '' - // textData = url - } - - $scope.log.unshift({ - 'name': '', - 'text': textData, - 'filename': filename - }) - - $scope.$apply() - break - case 'onOOBText': - var textData = data - $scope.chatLog.unshift({ - name: " > oob <", - text: $sce.trustAsHtml(textData) - }) - console.info('currResponse', textData) - $scope.$apply() - break + this.onMsg = function (inMsg) { + // console.info("ProgramABGui.onMsg(" + inMsg.method + ')') + let data = inMsg.data[0] + + switch (inMsg.method) { + case "onStatus": + $scope.status = data + $scope.$apply() + break + + case "onBotImage": + $scope.currentBotImage = data + $scope.$apply() + break + + case "onState": + _self.updateState(data) + $scope.$apply() + break + + case "onTopic": + $scope.service.currentTopic = data + $scope.$apply() + break + + case "onAimlFile": + $scope.aimlFileData.data = data + $scope.$apply() + break + + case "onPredicates": + $scope.predicates = data + $scope.$apply() + break + + case "onPredicate": + $scope.predicates[data.name] = data.value + $scope.$apply() + break + + case "onRequest": + var textData = data + $scope.chatLog.unshift({ + type: "User", + name: $scope.currentUserName, + text: $sce.trustAsHtml(textData), + }) + console.info("onRequest", textData) + $scope.$apply() + break + case "onResponse": + var textData = data + $scope.chatLog.unshift({ + type: "Bot", + name: $scope.service.config.botType, + text: $sce.trustAsHtml(data.msg), + }) + $scope.lastResponse = textData + $scope.$apply() + break + case "onLog": + var textData = data + let filename = null + parts = textData.split(" ") + if (parts.length > 2 && parts[1] == "Matched:") { + filename = parts[parts.length - 1] + textData = textData.substr(0, textData.lastIndexOf(" ") + 1) + // pos0 = textData.lastIndexOf(' ') + 1 + // url = textData.substr(0,pos0) + '' + // url = textData.substr(0,pos0) + '' + // textData = url + } + + $scope.log.unshift({ + name: "", + text: textData, + filename: filename, + }) + + $scope.$apply() + break + case "onOOBText": + var textData = data + $scope.chatLog.unshift({ + name: " > oob <", + text: $sce.trustAsHtml(textData), + }) + console.info("currResponse", textData) + $scope.$apply() + break default: - console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) - break - } + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } } - $scope.getAimlFile = function(filename) { - $scope.aimlFile = filename - console.log('getting aiml file ' + filename) - msg.send('getAimlFile', $scope.service.config.currentBotName, filename) - $scope.tabs.selected = 2 + $scope.getAimlFile = function (filename) { + $scope.aimlFile = filename + console.log("getting aiml file " + filename) + msg.send("getAimlFile", $scope.service.config.botType, filename) + $scope.tabs.selected = 2 } - $scope.saveAimlFile = function() { - msg.send("saveAimlFile", $scope.service.config.currentBotName, $scope.aimlFile, $scope.aimlFileData.data) + $scope.saveAimlFile = function () { + msg.send("saveAimlFile", $scope.service.config.botType, $scope.aimlFile, $scope.aimlFileData.data) } - $scope.setSessionKey = function() { - msg.send("setCurrentUserName", $scope.service.config.currentUserName) - msg.send("setCurrentBotName", $scope.service.config.currentBotName) + $scope.setSessionKey = function () { + msg.send("setCurrentUserName", $scope.service.config.username) + msg.send("setCurrentBotName", $scope.service.config.botType) } - $scope.getBotInfo = function() { - if ($scope.service && $scope.service.bots){ - return $scope.service.bots[$scope.service.config.currentBotName] - } - return null + $scope.getBotInfo = function () { + if ($scope.service && $scope.service.bots) { + return $scope.service.bots[$scope.service.config.botType] + } + return null } - $scope.getCurrentSession = function() { - if (!$scope.service.sessions){ - return null - } - if ($scope.getCurrentSessionKey()in $scope.service.sessions) { - return $scope.service.sessions[$scope.getCurrentSessionKey()] - } + $scope.getCurrentSession = function () { + if (!$scope.service.sessions) { return null + } + if ($scope.getCurrentSessionKey() in $scope.service.sessions) { + return $scope.service.sessions[$scope.getCurrentSessionKey()] + } + return null } - $scope.getCurrentSessionKey = function() { - return $scope.service.config.currentUserName + ' <-> ' + $scope.service.config.currentBotName + $scope.getCurrentSessionKey = function () { + return $scope.service.config.username + " <-> " + $scope.service.config.botType } - $scope.test = function(session, utterance) { - msg.send("getCategories", "hello") + $scope.test = function (session, utterance) { + msg.send("getCategories", "hello") } - $scope.getSessionResponse = function(utterance) { - console.info("SESSION GET RESPONSE (" + $scope.currentUserName + " " + $scope.service.config.currentBotName + ")") - $scope.getResponse($scope.currentUserName, $scope.service.config.currentBotName, utterance) + $scope.getSessionResponse = function (utterance) { + console.info("SESSION GET RESPONSE (" + $scope.currentUserName + " " + $scope.service.config.botType + ")") + $scope.getResponse($scope.currentUserName, $scope.service.config.botType, utterance) } - $scope.getResponse = function(username, botname, utterance) { - console.info("USER BOT RESPONSE (" + username + " " + botname + ")") - msg.send("getResponse", username, botname, utterance) - $scope.utterance = "" + $scope.getResponse = function (username, botname, utterance) { + console.info("USER BOT RESPONSE (" + username + " " + botname + ")") + msg.send("getResponse", username, botname, utterance) + $scope.utterance = "" } - $scope.startDialog = function() { - startDialog = $uibModal.open({ - templateUrl: "startDialog.html", - scope: $scope, - controller: function($scope) { - $scope.cancel = function() { - startDialog.dismiss() - } - - } - }) + $scope.startDialog = function () { + startDialog = $uibModal.open({ + templateUrl: "startDialog.html", + scope: $scope, + controller: function ($scope) { + $scope.cancel = function () { + startDialog.dismiss() + } + }, + }) } - $scope.startSession = function(username, botname) { - $scope.currentUserName = username - $scope.chatLog.unshift("Reload Session for Bot " + botname) - $scope.startSessionLabel = 'Reload Session' - msg.send("startSession", username, botname) - startDialog.dismiss() + $scope.startSession = function (username, botname) { + $scope.currentUserName = username + $scope.chatLog.unshift("Reload Session for Bot " + botname) + $scope.startSessionLabel = "Reload Session" + msg.send("startSession", username, botname) + startDialog.dismiss() } - $scope.savePredicates = function() { - $scope.service = mrl.getService($scope.service.name) - mrl.sendTo($scope.service.name, "savePredicates") - // FIXME !!!! lame + $scope.savePredicates = function () { + $scope.service = mrl.getService($scope.service.name) + mrl.sendTo($scope.service.name, "savePredicates") + // FIXME !!!! lame } - $scope.getProperties = function() { - if (!$scope.getBotInfo()){ - return null - } - return $scope.getBotInfo()['properties'] + $scope.getProperties = function () { + if (!$scope.getBotInfo()) { + return null + } + return $scope.getBotInfo()["properties"] } - $scope.getProperty = function(propName) { - try { - if ($scope.getBotInfo() && $scope.getBotInfo()['properties']){ - return $scope.getBotInfo()['properties'][propName] - } - } catch (error){ - console.warn('getProperty(' + propName + ') not found') - return null + $scope.getProperty = function (propName) { + try { + if ($scope.getBotInfo() && $scope.getBotInfo()["properties"]) { + return $scope.getBotInfo()["properties"][propName] } + } catch (error) { + console.warn("getProperty(" + propName + ") not found") + return null + } } - $scope.removeBotProperty = function(propName) { - delete $scope.getBotInfo()['properties'][propName] - msg.send("removeBotProperty", propName) + $scope.removeBotProperty = function (propName) { + delete $scope.getBotInfo()["properties"][propName] + msg.send("removeBotProperty", propName) } - $scope.aceLoaded = function(_editor) { - // _editor.setReadOnly(true); - $scope.aimlEditor = _editor - console.log('aceLoaded') + $scope.aceLoaded = function (_editor) { + // _editor.setReadOnly(true); + $scope.aimlEditor = _editor + console.log("aceLoaded") } - $scope.aceChanged = function(e) { - // - console.log('aceChanged') + $scope.aceChanged = function (e) { + // + console.log("aceChanged") } - $scope.getBotPath = function(e) { - if ($scope.service?.bots && $scope.service?.bots[$scope.service?.config?.currentBotName]?.path){ - return $scope.service?.bots[$scope.service?.config.currentBotName].path - } - return null + $scope.getBotPath = function (e) { + if ($scope.service?.bots && $scope.service?.bots[$scope.service?.config?.currentBotName]?.path) { + return $scope.service?.bots[$scope.service?.config.botType].path + } + return null } - - - $scope.getStatusLabel = function(level) { - if (level == 'error') { - return 'row label col-md-12 label-danger' - } - if (level == 'warn') { - return 'row label col-md-12 label-warning' - } - return 'row label col-md-12 label-info' + $scope.getStatusLabel = function (level) { + if (level == "error") { + return "row label col-md-12 label-danger" + } + if (level == "warn") { + return "row label col-md-12 label-warning" + } + + return "row label col-md-12 label-info" } // subscribe to the response from programab. - msg.subscribe('publishTopic') - msg.subscribe('publishRequest') - msg.subscribe('publishResponse') - msg.subscribe('publishLog') - msg.subscribe('publishOOBText') - msg.subscribe('getPredicates') - msg.subscribe('publishPredicate') - msg.subscribe('getAimlFile') + msg.subscribe("publishTopic") + msg.subscribe("publishRequest") + msg.subscribe("publishResponse") + msg.subscribe("publishLog") + msg.subscribe("publishOOBText") + msg.subscribe("getPredicates") + msg.subscribe("publishPredicate") + msg.subscribe("getAimlFile") + msg.send("getPredicates") - msg.send('getPredicates') - msg.subscribe(this) -} + }, ]) /* .filter('orderObjectBy', function() { diff --git a/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html b/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html index 8a9d2876c0..c8863b5fa5 100644 --- a/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html +++ b/src/main/resources/resource/WebGui/app/service/views/ProgramABGui.html @@ -19,17 +19,17 @@ - +
    - + - - + + - + @@ -41,7 +41,7 @@
    - +
    diff --git a/src/test/java/org/myrobotlab/service/ArduinoMotorPotTest.java b/src/test/java/org/myrobotlab/service/ArduinoMotorPotTest.java index 3493779590..ffe514e72c 100644 --- a/src/test/java/org/myrobotlab/service/ArduinoMotorPotTest.java +++ b/src/test/java/org/myrobotlab/service/ArduinoMotorPotTest.java @@ -74,7 +74,7 @@ private String join(ArrayList list, String joinChar) { public void onSensorData(SensorData event) { // about we downsample this call? - int[] data = (int[]) event.getData(); + int[] data = (int[]) event.data; count++; int value = data[0]; log.info("Data: {}", data); diff --git a/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml b/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml index 257d55e668..9a40847137 100644 --- a/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml +++ b/src/test/resources/ProgramAB/bots/lloyd/aiml/lloyd.aiml @@ -1,167 +1,273 @@ - - -* -