From e07d8b07ec8f2f885dcee5cb3b2197e21bf4c5ef Mon Sep 17 00:00:00 2001 From: supertick Date: Sat, 18 Mar 2023 20:31:28 -0700 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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;