From 829ccd1178ba69e5d034bf40e1558d164c2503ec Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 20 Jan 2024 05:25:57 -0800
Subject: [PATCH 001/131] model for aiml xml

---
 .../myrobotlab/programab/models/Event.java    | 65 +++++++++++++++++++
 .../org/myrobotlab/programab/models/Mrl.java  | 14 ++++
 .../org/myrobotlab/programab/models/Oob.java  | 14 ++++
 .../myrobotlab/programab/models/Sraix.java    | 10 +++
 .../myrobotlab/programab/models/Template.java | 51 +++++++++++++++
 5 files changed, 154 insertions(+)
 create mode 100644 src/main/java/org/myrobotlab/programab/models/Event.java
 create mode 100644 src/main/java/org/myrobotlab/programab/models/Mrl.java
 create mode 100644 src/main/java/org/myrobotlab/programab/models/Oob.java
 create mode 100644 src/main/java/org/myrobotlab/programab/models/Sraix.java
 create mode 100644 src/main/java/org/myrobotlab/programab/models/Template.java

diff --git a/src/main/java/org/myrobotlab/programab/models/Event.java b/src/main/java/org/myrobotlab/programab/models/Event.java
new file mode 100644
index 0000000000..93e85c4086
--- /dev/null
+++ b/src/main/java/org/myrobotlab/programab/models/Event.java
@@ -0,0 +1,65 @@
+package org.myrobotlab.programab.models;
+
+/**
+ * Pojo for state change of one of ProgramAB's state info
+ * @author GroG
+ *
+ */
+public class Event {
+  /**
+   * the botName in this state change - typically 
+   * current session botName
+   */
+  public String botname;
+  /**
+   * unique identifier for the session user & bot
+   */
+  public String id;
+  
+  /**
+   * name of the predicate changed
+   */
+  public String name;
+    
+  /**
+   * service this topic change came from
+   */
+  public String src;
+  
+  /**
+   * new topic or state name in this transition
+   */
+  public String topic;
+   
+  /**
+   * timestamp
+   */
+  public long ts = System.currentTimeMillis();
+  
+  /**
+   * the user name in this state change - usually
+   * current session userName
+   */
+  public String user;
+  
+  /**
+   * new value
+   */
+  public String value;
+  
+  public Event() {    
+  }
+  
+  public Event(String src, String userName, String botName, String topic) {
+    this.src = src;
+    this.user = userName;
+    this.botname = botName;
+    this.topic = topic;
+  }
+  
+  
+  @Override
+  public String toString() {
+    return String.format("%s %s=%s", id, name, value);
+  }
+}
diff --git a/src/main/java/org/myrobotlab/programab/models/Mrl.java b/src/main/java/org/myrobotlab/programab/models/Mrl.java
new file mode 100644
index 0000000000..04c1bf79bb
--- /dev/null
+++ b/src/main/java/org/myrobotlab/programab/models/Mrl.java
@@ -0,0 +1,14 @@
+package org.myrobotlab.programab.models;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+
+public class Mrl {
+  public String service;
+  public String method;
+  @JacksonXmlElementWrapper(useWrapping = false)
+  @JsonProperty("param")
+  public List<String> params;
+}
\ No newline at end of file
diff --git a/src/main/java/org/myrobotlab/programab/models/Oob.java b/src/main/java/org/myrobotlab/programab/models/Oob.java
new file mode 100644
index 0000000000..833bab5a0f
--- /dev/null
+++ b/src/main/java/org/myrobotlab/programab/models/Oob.java
@@ -0,0 +1,14 @@
+package org.myrobotlab.programab.models;
+
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+
+public class Oob {
+  
+  public String mrljson;
+  
+  @JacksonXmlElementWrapper(useWrapping = false)
+  public List<Mrl> mrl;
+}
+
diff --git a/src/main/java/org/myrobotlab/programab/models/Sraix.java b/src/main/java/org/myrobotlab/programab/models/Sraix.java
new file mode 100644
index 0000000000..99b0639cb6
--- /dev/null
+++ b/src/main/java/org/myrobotlab/programab/models/Sraix.java
@@ -0,0 +1,10 @@
+package org.myrobotlab.programab.models;
+
+// FIXME add attributes and internal tags
+public class Sraix {
+
+  public String search;
+
+  public Oob oob;
+  
+}
diff --git a/src/main/java/org/myrobotlab/programab/models/Template.java b/src/main/java/org/myrobotlab/programab/models/Template.java
new file mode 100644
index 0000000000..91f8e5de51
--- /dev/null
+++ b/src/main/java/org/myrobotlab/programab/models/Template.java
@@ -0,0 +1,51 @@
+package org.myrobotlab.programab.models;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
+
+//@JacksonXmlRootElement(localName = "template")
+//@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Template {
+  // @JacksonXmlElementWrapper(useWrapping = false)
+  
+  @JacksonXmlProperty(localName = "template")
+  
+  @JacksonXmlText
+  public String text;  
+  
+  
+public Oob oob;
+  
+//  @JsonProperty("ignorable")
+//  public List<Oob> oob;
+//
+//  public List<Oob> getOob() {
+//    return oob;
+//  }
+//
+//  public void setOob(List<Oob> oob) {
+//    this.oob = oob;
+//  }
+  
+  public static void main(String[] args) {
+
+    try {
+      
+      // String xml = "<template>XXX<oob><mrl><service>blah</service><method>method</method></mrl></oob></template>";
+      // String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method></mrl><mrl><service>blah2</service><method>method2</method></mrl></oob></template>";
+      String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method><param>p1</param><param>p2</param><param>p3</param></mrl><mrl><service>blah2</service><method>method2</method></mrl><mrljson>[\"method\":\"doIt\",\"data\":[\"p1\"]]</mrljson></oob></template>";
+      
+      XmlMapper xmlMapper = new XmlMapper();
+      Template template = xmlMapper.readValue(xml, Template.class);
+      
+      System.out.println(template);
+      
+    } catch(Exception e) {
+      e.printStackTrace();
+    }
+    }
+
+}

From 67bd304087adcb0f3e6fc2f88a86bcc2f31311f5 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 20 Jan 2024 07:48:16 -0800
Subject: [PATCH 002/131] InMoov gets a heart

---
 .../java/org/myrobotlab/service/InMoov2.java  | 622 +++++++++++++-----
 .../service/config/InMoov2Config.java         |   2 +-
 2 files changed, 441 insertions(+), 183 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 339aeaf107..462258a2ac 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -4,15 +4,23 @@
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.commons.io.FilenameUtils;
 import org.myrobotlab.framework.Message;
+import org.myrobotlab.framework.Peer;
 import org.myrobotlab.framework.Plan;
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
@@ -41,14 +49,73 @@
 import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
 import org.myrobotlab.service.interfaces.ServoControl;
 import org.myrobotlab.service.interfaces.Simulator;
+import org.myrobotlab.service.interfaces.SpeechListener;
 import org.myrobotlab.service.interfaces.SpeechRecognizer;
 import org.myrobotlab.service.interfaces.SpeechSynthesis;
 import org.myrobotlab.service.interfaces.TextListener;
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
 
-public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, TextListener, TextPublisher,
-    JoystickListener, LocaleProvider, IKJointAngleListener {
+public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
+
+  public class Heartbeat {
+    public long count = 0;
+    public long ts = System.currentTimeMillis();
+    public String state;
+    public List<LogEntry> errors;
+    double batteryLevel = 100;
+    public boolean isPirOn = false;
+
+    public Heartbeat(InMoov2 inmoov) {
+      this.state = inmoov.state;
+      this.errors = inmoov.errors;
+      this.count = inmoov.heartbeatCount;
+      this.isPirOn = inmoov.isPirOn;
+    }
+  }
+
+  public class Heart implements Runnable {
+    private final ReentrantLock lock = new ReentrantLock();
+    private Thread thread;
+
+    @Override
+    public void run() {
+      if (lock.tryLock()) {
+        try {
+          while (!Thread.currentThread().isInterrupted()) {
+            invoke("publishHeartbeat");
+            Thread.sleep(config.heartbeatInterval);
+          }
+        } catch (InterruptedException ignored) {
+          Thread.currentThread().interrupt();
+        } finally {
+          lock.unlock();
+          log.info("heart stopping");
+          thread = null;
+        }
+      }
+    }
+
+    public void start() {
+      if (thread == null) {
+        log.info("starting heart");
+        thread = new Thread(this, String.format("%s-heart", getName()));
+        thread.start();
+        config.heartbeat = true;
+      } else {
+        log.info("heart already started");
+      }
+    }
+
+    public void stop() {
+      if (thread != null) {
+        thread.interrupt();
+        config.heartbeat = false;
+      } else {
+        log.info("heart already stopped");
+      }
+    }
+  }
 
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
@@ -58,6 +125,7 @@ public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleL
 
   static String speechRecognizer = "WebkitSpeechRecognition";
 
+
   /**
    * This method will load a python file into the python interpreter.
    * 
@@ -68,6 +136,7 @@ public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleL
   @Deprecated /* use execScript - this doesn't handle resources correctly */
   public static boolean loadFile(String file) {
     File f = new File(file);
+    // FIXME cannot be casting to Python ! Py4j would break
     Python p = (Python) Runtime.getService("python");
     log.info("Loading  Python file {}", f.getAbsolutePath());
     if (p == null) {
@@ -97,17 +166,19 @@ public static boolean loadFile(String file) {
     return true;
   }
 
+  /**
+   * number of times waited in boot state
+   */
+  protected int bootCount = 0;
+
   protected transient ProgramAB chatBot;
 
   protected List<String> configList;
 
   /**
-   * Configuration from runtime has started. This is when runtime starts
-   * processing a configuration set for the first time since inmoov was started
+   * map of events or states to sounds
    */
-  protected boolean configStarted = false;
-
-  String currentConfigurationName = "default";
+  protected Map<String, String> customSoundMap = new TreeMap<>();
 
   protected transient SpeechRecognizer ear;
 
@@ -126,14 +197,31 @@ public static boolean loadFile(String file) {
 
   protected Set<String> gestures = new TreeSet<String>();
 
+  /**
+   * Prevents actions or events from happening when InMoov2 is first booted
+   */
+  private boolean hasBooted = false;
+
+  private transient final Heart heart = new Heart();
+
+  protected long heartbeatCount = 0;
+
+  protected boolean heartBeating = false;
+
   protected transient HtmlFilter htmlFilter;
 
   protected transient ImageDisplay imageDisplay;
 
+  protected boolean isPirOn = false;
+
+  protected boolean isSpeaking = false;
+
   protected String lastGestureExecuted;
 
   protected Long lastPirActivityTime;
 
+  protected String lastState = null;
+
   /**
    * supported locales
    */
@@ -149,10 +237,22 @@ public static boolean loadFile(String file) {
 
   protected transient Python python;
 
+  /**
+   * initial state - updated on any state change
+   */
+  String state = "boot";
+
+  protected long stateLastIdleTime = System.currentTimeMillis();
+
+  protected long stateLastRandomTime = System.currentTimeMillis();
+
   protected String voiceSelected;
 
+  protected Double batteryLevel = 100.0;
+
   public InMoov2(String n, String id) {
     super(n, id);
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -166,29 +266,12 @@ public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
 
-      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
-          "pt-PT", "tr-TR");
-
       if (c.locale != null) {
         setLocale(c.locale);
       } else {
         setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString()));
       }
 
-      loadAppsScripts();
-
-      loadInitScripts();
-
-      if (c.loadGestures) {
-        loadGestures();
-      }
-
-      if (c.heartbeat) {
-        startHeartbeat();
-      } else {
-        stopHeartbeat();
-      }
-
     } catch (Exception e) {
       error(e);
     }
@@ -215,6 +298,141 @@ public void attachTextPublisher(TextPublisher service) {
     subscribe(service.getName(), "publishText");
   }
 
+  /**
+   * At boot all services specified through configuration have started, or if no
+   * configuration has started minimally the InMoov2 service has started. During
+   * the processing of config and starting other services data will have
+   * accumulated, and at boot, some of data may now be inspected and processed
+   * in a synchronous single threaded way. With reporting after startup, vs
+   * during, other peer services are not needed (e.g. audioPlayer is no longer
+   * needed to be started "before" InMoov2 because when boot is called
+   * everything that is wanted has been started.
+   *
+   * This method gets called multiple times by the heart beat, it walks through
+   * required processing and effectively waits and tries again if not finished.
+   * While in "boot", nothing else should be allowed to process until this
+   * process is completed.
+   */
+  synchronized public void boot() {
+
+    Runtime runtime = Runtime.getInstance();
+
+    try {
+
+      if (hasBooted) {
+        log.warn("will not boot again");
+        return;
+      }
+
+      bootCount++;
+      log.info("boot count {}", bootCount);
+
+      // config has not finished processing yet..
+      if (runtime.isProcessingConfig()) {
+        log.warn("runtime still processing config set {}, waiting ....", runtime.getConfigName());
+        return;
+      }
+
+      // check all required services are completely started - or
+      // wait/return until they are
+
+      // there is not much point in running InMoov2 without its
+      // core dependencies - those dependencies are ProgramAB,
+      // FiniteStatemachine and Py4j/Python - so boot will not
+      // finish unless these services have loaded
+
+      // Although this exposes type, it does use startPeer
+      // which allows the potential of the user switching types of processors
+      // if the processor
+
+      // TODO - make Py4j without zombies and more robust
+      /**
+       * Py4j is not ready for primetime yet
+       * <pre>
+       
+      Py4j py4j = (Py4j) startPeer("py4j");
+      if (!py4j.isReady()) {
+        log.warn("{} not ready....", getPeerName("py4j"));
+        return;
+      }
+      String code = FileIO.toString(getResourceDir() + fs + "InMoov2.py");
+      py4j.exec(code);
+
+      </pre>
+      */
+
+      // TODO - MAKE BOOT REPORT !!!! deliver it on a heartbeat
+      runtime.invoke("publishConfigList");
+      // FIXME - reduce the number of these
+      if (config.loadAppsScripts) {
+        loadAppsScripts();
+      }
+
+      if (config.loadInitScripts) {
+        loadInitScripts();
+      }
+
+      if (config.loadGestures) {
+        loadGestures();
+      }
+
+      if (config.startupSound) {
+        String startupsound = FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3");
+        invoke("publishPlayAudioFile", startupsound);
+      }
+
+      List<ServiceInterface> services = Runtime.getServices();
+      for (ServiceInterface si : services) {
+        if ("Servo".equals(si.getSimpleName())) {
+          send(si.getFullName(), "setAutoDisable", true);
+        }
+      }
+      hasBooted = true;
+    } catch (Exception e) {
+      hasBooted = false;
+      error(e);
+    }
+
+    /**
+     * TODO reporting on errors found in boot process TODO make a report on all
+     * peers that have started, of the config processed if there was a config
+     * set
+     */
+    if (config.reportOnBoot) {
+      systemEvent("CONFIG STARTED %s", runtime.getConfigName());
+
+      // TODO spin through all services in the order they were started
+      // send all system events
+      Collection<ServiceInterface> local = Runtime.getLocalServices().values();
+      List<ServiceInterface> ordered = new ArrayList<>(local);
+      ordered.removeIf(Objects::isNull);
+      Collections.sort(ordered);
+
+      Map<String, Peer> peers = getPeers();
+      Set<String> peerNames = new HashSet<>();
+      for (String peerKey : peers.keySet()) {
+        Peer peer = peers.get(peerKey);
+        if (peer.name == null) {
+          peerNames.add(String.format("%s.%s", getName(), peerKey));
+        } else {
+          peerNames.add(peer.name);
+        }
+      }
+
+      for (ServiceInterface si : ordered) {
+        if (peerNames.contains(si.getName())) {
+          systemEvent("STARTED %s", getPeerKey(si.getName()).replace(".", " "));
+        }
+      }
+
+      // reporting on all services and config started
+      systemEvent("CONFIG LOADED %s", runtime.getConfigName());
+    }
+
+    // say finished booting
+    fire("wake");
+  }
+
   public void beginCheckingOnInactivity() {
     beginCheckingOnInactivity(maxInactivityTimeSeconds);
   }
@@ -584,6 +802,14 @@ public InMoov2Hand getRightHand() {
     return (InMoov2Hand) getPeer("rightHand");
   }
 
+  public String getState() {
+    FiniteStateMachine fsm = (FiniteStateMachine) getPeer("fsm");
+    if (fsm == null) {
+      return null;
+    }
+    return fsm.getCurrent();
+  }
+
   /**
    * matches on language only not variant expands language match to full InMoov2
    * bot locale
@@ -870,6 +1096,11 @@ public void onCreated(String fullname) {
     log.info("{} created", fullname);
   }
 
+  @Override
+  public void onEndSpeaking(String utterance) {
+    isSpeaking = false;
+  }
+
   public void onFinishedConfig(String configName) {
     log.info("onFinishedConfig");
     // invoke("publishEvent", "configFinished");
@@ -885,6 +1116,10 @@ public void onGestureStatus(Status status) {
     unsubscribe("python", "publishStatus", this.getName(), "onGestureStatus");
   }
 
+  /**
+   * Central hub of input motion control. Potentially, all input from joysticks,
+   * quest2 controllers and headset, or any IK service could be sent here
+   */
   @Override
   public void onJointAngles(Map<String, Double> angleMap) {
     log.debug("onJointAngles {}", angleMap);
@@ -951,7 +1186,7 @@ public OpenCVData onOpenCVData(OpenCVData data) {
    * @param volume
    */
   public void onPeak(double volume) {
-    if (config.neoPixelFlashWhenSpeaking && !configStarted) {
+    if (config.neoPixelFlashWhenSpeaking && !"boot".equals(getState())) {
       if (volume > 0.5) {
         invoke("publishSpeakingFlash", "speaking");
       }
@@ -1031,121 +1266,10 @@ public void onStartConfig(String configName) {
    */
   @Override
   public void onStarted(String name) {
-    InMoov2Config c = (InMoov2Config) config;
-
-    log.info("onStarted {}", name);
     try {
 
-      Runtime runtime = Runtime.getInstance();
       log.info("onStarted {}", name);
-
-      // BAD IDEA - better to ask for a system report or an error report
-      // if (runtime.isProcessingConfig()) {
-      // invoke("publishEvent", "CONFIG STARTED");
-      // }
-
-      String peerKey = getPeerKey(name);
-      if (peerKey == null) {
-        // service not a peer
-        return;
-      }
-
-      if (runtime.isProcessingConfig() && !configStarted) {
-        invoke("publishEvent", "CONFIG STARTED " + runtime.getConfigName());
-        configStarted = true;
-      }
-
-      invoke("publishEvent", "STARTED " + peerKey);
-
-      switch (peerKey) {
-        case "audioPlayer":
-          break;
-        case "chatBot":
-          ProgramAB chatBot = (ProgramAB) Runtime.getService(name);
-          chatBot.attachTextListener(getPeerName("htmlFilter"));
-          startPeer("htmlFilter");
-          break;
-        case "controller3":
-          break;
-        case "controller4":
-          break;
-        case "ear":
-          AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name);
-          ear.attachTextListener(getPeerName("chatBot"));
-          break;
-        case "eyeTracking":
-          break;
-        case "fsm":
-          break;
-        case "gpt3":
-          break;
-        case "head":
-          addListener("publishMoveHead", name);
-          break;
-        case "headTracking":
-          break;
-        case "htmlFilter":
-          TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name);
-          htmlFilter.attachTextListener(getPeerName("mouth"));
-          break;
-        case "imageDisplay":
-          break;
-        case "leap":
-          break;
-        case "left":
-          break;
-        case "leftArm":
-          addListener("publishMoveLeftArm", name, "onMoveArm");
-          break;
-        case "leftHand":
-          addListener("publishMoveLeftHand", name, "onMoveHand");
-          break;
-        case "mouth":
-          mouth = (AbstractSpeechSynthesis) Runtime.getService(name);
-          mouth.attachSpeechListener(getPeerName("ear"));
-          break;
-        case "mouthControl":
-          break;
-        case "neoPixel":
-          break;
-        case "opencv":
-          subscribeTo(name, "publishOpenCVData");
-          break;
-        case "openni":
-          break;
-        case "openWeatherMap":
-          break;
-        case "pid":
-          break;
-        case "pir":
-          break;
-        case "random":
-          break;
-        case "right":
-          break;
-        case "rightArm":
-          addListener("publishMoveRightArm", name, "onMoveArm");
-          break;
-        case "rightHand":
-          addListener("publishMoveRightHand", name, "onMoveHand");
-          break;
-        case "servoMixer":
-          break;
-        case "simulator":
-          break;
-        case "torso":
-          addListener("publishMoveTorso", name);
-          break;
-        case "ultrasonicRight":
-          break;
-        case "ultrasonicLeft":
-          break;
-        default:
-          log.warn("unknown peer %s not hanled in onStarted", peerKey);
-          break;
-      }
-
-      // type processing for Servo
+      // new servo
       ServiceInterface si = Runtime.getService(name);
       if ("Servo".equals(si.getSimpleName())) {
         log.info("sending setAutoDisable true to {}", name);
@@ -1158,8 +1282,52 @@ public void onStarted(String name) {
     }
   }
 
+  // FIXME - rebroadcast these
+  @Override
+  public void onStartSpeaking(String utterance) {
+    isSpeaking = true;
+  }
+
+  /**
+   * The integration between the FiniteStateMachine (fsm) and the InMoov2
+   * service and potentially other services (Python, ProgramAB) happens here.
+   * 
+   * After boot all state changes get published here.
+   * 
+   * Some InMoov2 service methods will be called here for "default
+   * implemenation" of states. If a user doesn't want to have that default
+   * implementation, they can change it by changing the definition of the state
+   * machine, and have a new state which will call a Python inmoov2 library
+   * callback. Overriding, appending, or completely transforming the behavior is
+   * all easily accomplished by managing the fsm and python inmoov2 library
+   * callbacks.
+   * 
+   * Python inmoov2 callbacks ProgramAB topic switching
+   * 
+   * Depending on config:
+   * 
+   * 
+   * @param stateChange
+   * @return
+   */
+  public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) {
+    try {
+      log.info("onStateChange {}", stateChange);
+
+      lastState = state;
+      state = stateChange.state;
+
+      processMessage("onStateChange", stateChange);
+
+    } catch (Exception e) {
+      error(e);
+    }
+    return stateChange;
+  }
+
   @Override
   public void onStopped(String name) {
+    log.info("service {} has stopped");
     // using release peer for peer releasing
     // FIXME - auto remove subscriptions of peers?
   }
@@ -1262,9 +1430,102 @@ public String publishFlash(String flashName) {
     return flashName;
   }
 
-  public String publishHeartbeat() {
-    invoke("publishFlash", "heartbeat");
-    return getName();
+  /**
+   * A heartbeat that continues to check status, and fire events to the FSM.
+   * Checks battery, flashes leds and processes all the configured checks in
+   * onHeartbeat at a regular interval
+   */
+  public Heartbeat publishHeartbeat() {
+    log.debug("publishHeartbeat");
+    heartbeatCount++;
+    Heartbeat heartbeat = new Heartbeat(this);
+    try {
+
+      if ("boot".equals(state)) {
+        // continue booting - we don't put heartbeats in user/python space
+        // until java-land is done booting
+        log.info("boot hasn't completed, will not process heartbeat");
+        boot();
+        return heartbeat;
+      }
+
+      Long lastActivityTime = getLastActivityTime();
+
+      // FIXME lastActivityTime != 0 is bogus - the value should be null if
+      // never set
+      if (config.stateIdleInterval != null && lastActivityTime != null && lastActivityTime != 0
+          && lastActivityTime + (config.stateIdleInterval * 1000) < System.currentTimeMillis()) {
+        stateLastIdleTime = lastActivityTime;
+      }
+
+      if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) {
+        fsm.fire("idle");
+        stateLastIdleTime = System.currentTimeMillis();
+      }
+
+      // interval event firing
+      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+        // fsm.fire("random");
+        stateLastRandomTime = System.currentTimeMillis();
+      }
+
+    } catch (Exception e) {
+      error(e);
+    }
+
+    // if (config.pirOnFlash && isPeerStarted("pir") && isPirOn) {
+    //// flash("pir");
+    // }
+
+    if (config.batteryInSystem) {
+      double batteryLevel = Runtime.getBatteryLevel();
+      invoke("publishBatteryLevel", batteryLevel);
+      // FIXME - thresholding should always have old value or state
+      // so we don't pump endless errors
+      if (batteryLevel < 5) {
+        error("battery level < 5 percent");
+        // systemEvent(BATTERY ERROR)
+      } else if (batteryLevel < 10) {
+        warn("battery level < 10 percent");
+        // systemEvent(BATTERY WARN)
+      }
+    }
+
+    // flash error until errors are cleared
+    if (config.flashOnErrors) {
+      if (errors.size() > 0) {
+        // invoke("publishFlash", "error");
+      } else {
+        // invoke("publishFlash", "heartbeat");
+      }
+    }
+
+    // FIXME - add errors to heartbeat
+    processMessage("onHeartbeat", heartbeat);
+    return heartbeat;
+  }
+
+  public void processMessage(String method) {
+    processMessage(method, null);
+  }
+
+  /**
+   * Will publish processing messages to the processor(s) currently subscribed.
+   * 
+   * @param method
+   * @param data
+   */
+  public void processMessage(String method, Object data) {
+    // User processing should not occur until after boot has completed
+    if (!state.equals("boot")) {
+      // FIXME - this needs to be in config
+      // FIXME - change peer name to "processor"
+      String processor = getPeerName("py4j");
+      Message msg = Message.createMessage(getName(), processor, method, data);
+      // FIXME - is this too much abstraction .. to publish as well as
+      // configurable send ?
+      invoke("publishProcessMessage", msg);
+    }
   }
 
   /**
@@ -1393,6 +1654,7 @@ public void releasePeer(String peerKey) {
   public void releaseService() {
     try {
       disable();
+      heart.stop();
       super.releaseService();
     } catch (Exception e) {
       error(e);
@@ -1791,7 +2053,7 @@ public void startedGesture(String nameOfGesture) {
   }
 
   public void startHeartbeat() {
-    addTask(1000, "publishHeartbeat");
+    heart.start();
   }
 
   // TODO - general objective "might" be to reduce peers down to something
@@ -1852,57 +2114,42 @@ public ServiceInterface startPeer(String peer) {
   @Override
   public void startService() {
     super.startService();
-
+    // FIXME - hardcoded peer no choice of type
+    fsm = (FiniteStateMachine) startPeer("fsm");
+
+    // a python processor is
+    // necessary for InMoov2 to properly
+    // function but this is not the place to start it
+    // it should be a peer definition too, it can be "python"
+    // it doesn't need to be i01.python to be a peer
+    // also should determine type Py4j or Python
+    // Runtime.start("python");
+
+    // just for comparing config with current "default"
+    // debugging only
     Runtime runtime = Runtime.getInstance();
 
+    // if you hardcode subscriptions here - they should
+    // be controlled/branched by config
+
     // get service start and release life cycle events
     runtime.attachServiceLifeCycleListener(getName());
 
-    List<ServiceInterface> services = Runtime.getServices();
-    for (ServiceInterface si : services) {
-      if ("Servo".equals(si.getSimpleName())) {
-        send(si.getFullName(), "setAutoDisable", true);
-      }
-    }
-
+    // FIXME all subscriptions should be in InMoov2Config
     // get events of new services and shutdown
+    // we can't add listener's in config, perhaps there should be
+    // "subscriptions" in config too ?
     subscribe("runtime", "shutdown");
-    // power up loopback subscription
-    addListener(getName(), "powerUp");
-
     subscribe("runtime", "publishConfigList");
-    if (runtime.isProcessingConfig()) {
-      invoke("publishEvent", "configStarted");
-    }
-    subscribe("runtime", "publishConfigStarted");
-    subscribe("runtime", "publishConfigFinished");
 
-    // chatbot getresponse attached to publishEvent
-    addListener("publishEvent", getPeerName("chatBot"), "getResponse");
+    runtime.invoke("publishConfigList");
 
-    try {
-      // copy config if it doesn't already exist
-      String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config");
-      List<File> files = FileIO.getFileList(resourceBotDir);
-      for (File f : files) {
-        String botDir = "data/config/" + f.getName();
-        File bDir = new File(botDir);
-        if (bDir.exists() || !f.isDirectory()) {
-          log.info("skipping data/config/{}", botDir);
-        } else {
-          log.info("will copy new data/config/{}", botDir);
-          try {
-            FileIO.copy(f.getAbsolutePath(), botDir);
-          } catch (Exception e) {
-            error(e);
-          }
-        }
-      }
-    } catch (Exception e) {
-      error(e);
+    if (config.heartbeat) {
+      startHeartbeat();
+    } else {
+      stopHeartbeat();
     }
 
-    runtime.invoke("publishConfigList");
   }
 
   public void startServos() {
@@ -1930,12 +2177,13 @@ public void stop() {
   }
 
   public void stopGesture() {
+    // FIXME cannot be casting to Python
     Python p = (Python) Runtime.getService("python");
     p.stop();
   }
 
   public void stopHeartbeat() {
-    purgeTask("publishHeartbeat");
+    heart.stop();
   }
 
   public void stopNeopixelAnimation() {
@@ -1962,7 +2210,17 @@ public void systemCheck() {
     Platform platform = Runtime.getPlatform();
     setPredicate("system version", platform.getVersion());
     // ERROR buffer !!!
-    invoke("publishEvent", "systemCheckFinished");
+    systemEvent("SYSTEMCHECKFINISHED"); // wtf is this?
+  }
+
+  public String systemEvent(String eventMsg) {
+    invoke("publishSystemEvent", eventMsg);
+    return eventMsg;
+  }
+
+  public String systemEvent(String format, Object... ags) {
+    String eventMsg = String.format(format, ags);
+    return systemEvent(eventMsg);
   }
 
   // FIXME - if this is really desired it will drive local references for all
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 564fe516e4..3c9571e435 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -55,7 +55,7 @@ public class InMoov2Config extends ServiceConfig {
    * fire events to the FSM. Checks battery level and sends a heartbeat flash on
    * publishHeartbeat and onHeartbeat at a regular interval
    */
-  public boolean heartbeat = false;
+  public boolean heartbeat = true;
 
   /**
    * flashes the neopixel every time a health check is preformed. green == good

From 754bed4f764ac802d8643ea841184573e4be0105 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 20 Jan 2024 13:38:40 -0800
Subject: [PATCH 003/131] jython worky with InMoov2.py

---
 .../java/org/myrobotlab/service/InMoov2.java  | 104 ++++++++++++++----
 .../service/config/InMoov2Config.java         |   8 +-
 2 files changed, 90 insertions(+), 22 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 462258a2ac..cba05f550f 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -58,7 +58,7 @@
 
 public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
 
-  public class Heartbeat {
+  public static class Heartbeat {
     public long count = 0;
     public long ts = System.currentTimeMillis();
     public String state;
@@ -360,6 +360,10 @@ synchronized public void boot() {
 
       </pre>
       */
+      
+      // load the InMoov2.py and publish it for Python/Jython or Py4j to consume
+      String script = getResourceAsString("InMoov2.py");
+      invoke("publishPython", script);
 
       // TODO - MAKE BOOT REPORT !!!! deliver it on a heartbeat
       runtime.invoke("publishConfigList");
@@ -730,30 +734,32 @@ public InMoov2Head getHead() {
    * @return the timestamp of the last activity time.
    */
   public Long getLastActivityTime() {
-    try {
-
-      Long lastActivityTime = 0L;
-
-      Long head = (Long) sendToPeerBlocking("head", "getLastActivityTime", getName());
-      Long leftArm = (Long) sendToPeerBlocking("leftArm", "getLastActivityTime", getName());
-      Long rightArm = (Long) sendToPeerBlocking("rightArm", "getLastActivityTime", getName());
-      Long leftHand = (Long) sendToPeerBlocking("leftHand", "getLastActivityTime", getName());
-      Long rightHand = (Long) sendToPeerBlocking("rightHand", "getLastActivityTime", getName());
-      Long torso = (Long) sendToPeerBlocking("torso", "getLastActivityTime", getName());
-
-      lastActivityTime = Math.max(head, leftArm);
+    Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
+
+    Long lastActivityTime = null;
+
+    if (head != null || leftArm != null || rightArm != null || leftHand != null || rightHand != null || torso != null) {
+      lastActivityTime = 0L;
+      if (head != null)
+        lastActivityTime = Math.max(lastActivityTime, head);
+      if (leftArm != null)
+        lastActivityTime = Math.max(lastActivityTime, leftArm);
+      if (rightArm != null)
       lastActivityTime = Math.max(lastActivityTime, rightArm);
+      if (leftHand != null)
       lastActivityTime = Math.max(lastActivityTime, leftHand);
+      if (rightHand != null)
       lastActivityTime = Math.max(lastActivityTime, rightHand);
+      if (torso != null)
       lastActivityTime = Math.max(lastActivityTime, torso);
-
-      return lastActivityTime;
-
-    } catch (Exception e) {
-      error(e);
-      return null;
     }
 
+      return lastActivityTime;
   }
 
   public InMoov2Arm getLeftArm() {
@@ -1520,7 +1526,9 @@ public void processMessage(String method, Object data) {
     if (!state.equals("boot")) {
       // FIXME - this needs to be in config
       // FIXME - change peer name to "processor"
-      String processor = getPeerName("py4j");
+      // String processor = getPeerName("py4j");
+      String processor = "python";
+      
       Message msg = Message.createMessage(getName(), processor, method, data);
       // FIXME - is this too much abstraction .. to publish as well as
       // configurable send ?
@@ -1634,6 +1642,62 @@ public HashMap<String, Double> publishMoveTorso(Double topStom, Double midStom,
     return map;
   }
 
+  public String publishPlayAudioFile(String filename) {
+    return filename;
+  }
+
+  /**
+   * Processing publishing point, where everything InMoov2 wants to be processed
+   * is turned into a message and published.
+   * 
+   * @param msg
+   * @return
+   */
+  public Message publishProcessMessage(Message msg) {
+    return msg;
+  }
+
+  /**
+   * Possible pub/sub way to interface with python - no blocking though
+   * 
+   * @param code
+   * @return
+   */
+  public String publishPython(String code) {
+    return code;
+  }
+
+
+  /**
+   * publishes a name for NeoPixel.onFlash to consume, in a seperate channel to
+   * potentially be used by "speaking only" leds
+   * 
+   * @param name
+   * @return
+   */
+  public String publishSpeakingFlash(String name) {
+    return name;
+  }
+
+  /**
+   * stop animation event
+   */
+  public void publishStopAnimation() {
+  }
+
+  /**
+   * event publisher for the fsm - although other services potentially can
+   * consume and filter this event channel
+   * 
+   * @param event
+   * @return
+   */
+  public String publishSystemEvent(String event) {
+    // well, it turned out underscore was a goofy selection, as underscore in
+    // aiml is wildcard ... duh
+    return String.format("SYSTEM_EVENT %s", event);
+  }
+
   /**
    * all published text from InMoov2 - including ProgramAB
    */
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 3c9571e435..014cfcf0e9 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -188,7 +188,7 @@ public Plan getDefault(Plan plan, String name) {
     addDefaultPeerConfig(plan, name, "openWeatherMap", "OpenWeatherMap", false);
     addDefaultPeerConfig(plan, name, "pid", "Pid", false);
     addDefaultPeerConfig(plan, name, "pir", "Pir", false);
-    addDefaultPeerConfig(plan, name, "py4j", "Py4j", true);
+    addDefaultPeerConfig(plan, name, "py4j", "Py4j", false);
     addDefaultPeerConfig(plan, name, "random", "Random", false);
     addDefaultPeerConfig(plan, name, "right", "Arduino", false);
     addDefaultPeerConfig(plan, name, "rightArm", "InMoov2Arm", false);
@@ -519,7 +519,11 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer")));
     listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel")));
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
-    listeners.add(new Listener("publishProcessMessage", getPeerName("py4j"), "onPythonMessage"));
+    // listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
+    listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
+    listeners.add(new Listener("publishPython", "python"));
+    
+    
 
     // InMoov2 --to--> InMoov2
     listeners.add(new Listener("publishMoveHead", name));

From ed459dafbc7da4867a86e7867e99f3c97c6b3487 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 20 Jan 2024 20:56:11 -0800
Subject: [PATCH 004/131] reset

---
 .../org/myrobotlab/service/RandomTest.java    | 55 ++++++++++---------
 1 file changed, 30 insertions(+), 25 deletions(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 9c3739f510..1e42ab9e3a 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -17,24 +17,23 @@ public class RandomTest extends AbstractServiceTest {
              * rarely happens - seems not useful and silly
              */
   public Service createService() throws Exception {
-    return (Service) Runtime.start("randomTest", "Random");
+    return (Service) Runtime.start("random", "Random");
   }
-  
+
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-      // clean our config directory
-    // Runtime.removeConfig("RandomTest");
+    // clean our config directory
+    Runtime.removeConfig("RandomTest");
     // set our config
     Runtime.setConfig("RandomTest");
   }
-  
 
   @Override
   public void testService() throws Exception {
     Clock clock = (Clock) Runtime.start("clock", "Clock");
-    Random random = (Random) Runtime.start("randomTest", "Random");
+    Random random = (Random) Runtime.start("random", "Random");
 
     clock.stopClock();
     clock.setInterval(1000);
@@ -43,65 +42,71 @@ public void testService() throws Exception {
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.enable();
 
-    sleep(1000);
+    sleep(500);
 
     assertTrue("should have method", random.getKeySet().contains("clock.setInterval"));
-    
-    assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 1 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
-    
+
+    assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()),
+        5000 <= clock.getInterval());
+    assertTrue(String.format("random method 1 should be %d <= 10000 values", clock.getInterval()),
+        clock.getInterval() <= 10000);
+
     random.remove("clock.setInterval");
-    
+
     assertTrue("should not have method", !random.getKeySet().contains("clock.setInterval"));
 
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.addRandom(0, 200, "clock", "startClock");
-    
+
     sleep(500);
     assertTrue("clock should be started 1", clock.isClockRunning());
-    
+
     // disable all of a services random events
     random.disable("clock.startClock");
     clock.stopClock();
     sleep(250);
     assertTrue("clock should not be started 1", !clock.isClockRunning());
-    
+
     // enable all of a service's random events
     random.enable("clock.startClock");
     sleep(250);
     assertTrue("clock should be started 2", clock.isClockRunning());
-    
+
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
     clock.setInterval(999999);
     sleep(200);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
-    assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 2 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()),
+        5000 <= clock.getInterval());
+    assertTrue(String.format("random method 2 should be %d <= 10000 values", clock.getInterval()),
+        clock.getInterval() <= 10000);
 
     // disable all
     random.disable();
     sleep(200);
     clock.setInterval(999999);
-    assertTrue("clock should not be started 4", !clock.isClockRunning());   
-    assertEquals(999999, (long)clock.getInterval());
+    assertTrue("clock should not be started 4", !clock.isClockRunning());
+    assertEquals(999999, (long) clock.getInterval());
 
     // re-enable all that were previously enabled but not explicitly disabled ones
     random.enable();
     sleep(1000);
     assertTrue("clock should not be started 5", !clock.isClockRunning());
-    assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()),
+        5000 <= clock.getInterval());
+    assertTrue(String.format("random method 3 should be %d <= 10000 values", clock.getInterval()),
+        clock.getInterval() <= 10000);
 
     clock.stopClock();
     random.purge();
-        
+
     Map<String, RandomMessage> events = random.getRandomEvents();
     assertTrue(events.size() == 0);
-    
+
     random.addRandom("named task", 200, 500, "clock", "setInterval", 100, 1000, 10);
-    
+
     clock.releaseService();
     random.releaseService();
 

From e0d1687d9aa144fc3bbab7c68f1e3ad649edf67c Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 06:47:48 -0800
Subject: [PATCH 005/131] ordered and formatted

---
 .../java/org/myrobotlab/service/InMoov2.java  | 567 +++++++++---------
 .../service/config/InMoov2Config.java         |   8 +-
 2 files changed, 272 insertions(+), 303 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index cba05f550f..caa06027f3 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -12,7 +12,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -36,7 +35,6 @@
 import org.myrobotlab.programab.PredicateEvent;
 import org.myrobotlab.programab.Response;
 import org.myrobotlab.service.Log.LogEntry;
-import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
 import org.myrobotlab.service.config.OpenCVConfig;
@@ -56,23 +54,8 @@
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
 
-public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
-
-  public static class Heartbeat {
-    public long count = 0;
-    public long ts = System.currentTimeMillis();
-    public String state;
-    public List<LogEntry> errors;
-    double batteryLevel = 100;
-    public boolean isPirOn = false;
-
-    public Heartbeat(InMoov2 inmoov) {
-      this.state = inmoov.state;
-      this.errors = inmoov.errors;
-      this.count = inmoov.heartbeatCount;
-      this.isPirOn = inmoov.isPirOn;
-    }
-  }
+public class InMoov2 extends Service<InMoov2Config>
+    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
 
   public class Heart implements Runnable {
     private final ReentrantLock lock = new ReentrantLock();
@@ -117,6 +100,22 @@ public void stop() {
     }
   }
 
+  public static class Heartbeat {
+    double batteryLevel = 100;
+    public long count = 0;
+    public List<LogEntry> errors;
+    public boolean isPirOn = false;
+    public String state;
+    public long ts = System.currentTimeMillis();
+
+    public Heartbeat(InMoov2 inmoov) {
+      this.state = inmoov.state;
+      this.errors = inmoov.errors;
+      this.count = inmoov.heartbeatCount;
+      this.isPirOn = inmoov.isPirOn;
+    }
+  }
+
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
   public static LinkedHashMap<String, String> lpVars = new LinkedHashMap<String, String>();
@@ -125,12 +124,11 @@ public void stop() {
 
   static String speechRecognizer = "WebkitSpeechRecognition";
 
-
   /**
    * This method will load a python file into the python interpreter.
    * 
    * @param file
-   *             file to load
+   *          file to load
    * @return success/failure
    */
   @Deprecated /* use execScript - this doesn't handle resources correctly */
@@ -166,6 +164,102 @@ public static boolean loadFile(String file) {
     return true;
   }
 
+  public static void main(String[] args) {
+    try {
+
+      LoggingFactory.init(Level.ERROR);
+      // Platform.setVirtual(true);
+      // Runtime.start("s01", "Servo");
+      // Runtime.start("intro", "Intro");
+
+      Runtime.startConfig("dev");
+
+      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
+      // webgui.setSsl(true);
+      webgui.autoStartBrowser(false);
+      // webgui.setPort(8888);
+      webgui.startService();
+      InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+
+      boolean done = true;
+      if (done) {
+        return;
+      }
+
+      OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {
+      });
+      ocvConfig.flip = true;
+      i01.setPeerConfigValue("opencv", "flip", true);
+      // i01.savePeerConfig("", null);
+
+      // Runtime.startConfig("default");
+
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
+      // "WebGui",
+      // "intro", "Intro", "python", "Python" });
+
+      Runtime.start("python", "Python");
+      // Runtime.start("ros", "Ros");
+      Runtime.start("intro", "Intro");
+      // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+      // i01.startPeer("simulator");
+      // Runtime.startConfig("i01-05");
+      // Runtime.startConfig("pir-01");
+
+      // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly");
+      // i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+
+      // polly.speakBlocking("Hi, to be or not to be that is the question,
+      // wheather to take arms against a see of trouble, and by aposing them end
+      // them, to sleep, to die");
+      // i01.startPeer("mouth");
+      // i01.speakBlocking("Hi, to be or not to be that is the question,
+      // wheather to take arms against a see of trouble, and by aposing them end
+      // them, to sleep, to die");
+
+      Runtime.start("python", "Python");
+
+      // i01.startSimulator();
+      Plan plan = Runtime.load("webgui", "WebGui");
+      // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui");
+      // webgui.autoStartBrowser = false;
+      Runtime.startConfig("webgui");
+      Runtime.start("webgui", "WebGui");
+
+      Random random = (Random) Runtime.start("random", "Random");
+
+      random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+
+      random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
+      random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
+
+      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+
+      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0);
+      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0);
+
+      random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
+      random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
+
+      random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0);
+      random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0);
+
+      random.save();
+
+      // i01.startChatBot();
+      //
+      // i01.startAll("COM3", "COM4");
+      Runtime.start("python", "Python");
+
+    } catch (Exception e) {
+      log.error("main threw", e);
+    }
+  }
+
+  protected Double batteryLevel = 100.0;
+
   /**
    * number of times waited in boot state
    */
@@ -248,8 +342,6 @@ public static boolean loadFile(String file) {
 
   protected String voiceSelected;
 
-  protected Double batteryLevel = 100.0;
-
   public InMoov2(String n, String id) {
     super(n, id);
     locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
@@ -298,6 +390,18 @@ public void attachTextPublisher(TextPublisher service) {
     subscribe(service.getName(), "publishText");
   }
 
+  public void beginCheckingOnInactivity() {
+    beginCheckingOnInactivity(maxInactivityTimeSeconds);
+  }
+
+  public void beginCheckingOnInactivity(int maxInactivityTimeSeconds) {
+    this.maxInactivityTimeSeconds = maxInactivityTimeSeconds;
+    // speakBlocking("power down after %s seconds inactivity is on",
+    // this.maxInactivityTimeSeconds);
+    log.info("power down after %s seconds inactivity is on", this.maxInactivityTimeSeconds);
+    addTask("checkInactivity", 5 * 1000, 0, "checkInactivity");
+  }
+
   /**
    * At boot all services specified through configuration have started, or if no
    * configuration has started minimally the InMoov2 service has started. During
@@ -348,19 +452,20 @@ synchronized public void boot() {
       // TODO - make Py4j without zombies and more robust
       /**
        * Py4j is not ready for primetime yet
+       * 
        * <pre>
-       
-      Py4j py4j = (Py4j) startPeer("py4j");
-      if (!py4j.isReady()) {
-        log.warn("{} not ready....", getPeerName("py4j"));
-        return;
-      }
-      String code = FileIO.toString(getResourceDir() + fs + "InMoov2.py");
-      py4j.exec(code);
+       * 
+       * Py4j py4j = (Py4j) startPeer("py4j");
+       * if (!py4j.isReady()) {
+       *   log.warn("{} not ready....", getPeerName("py4j"));
+       *   return;
+       * }
+       * String code = FileIO.toString(getResourceDir() + fs + "InMoov2.py");
+       * py4j.exec(code);
+       * 
+       * </pre>
+       */
 
-      </pre>
-      */
-      
       // load the InMoov2.py and publish it for Python/Jython or Py4j to consume
       String script = getResourceAsString("InMoov2.py");
       invoke("publishPython", script);
@@ -437,18 +542,6 @@ synchronized public void boot() {
     fire("wake");
   }
 
-  public void beginCheckingOnInactivity() {
-    beginCheckingOnInactivity(maxInactivityTimeSeconds);
-  }
-
-  public void beginCheckingOnInactivity(int maxInactivityTimeSeconds) {
-    this.maxInactivityTimeSeconds = maxInactivityTimeSeconds;
-    // speakBlocking("power down after %s seconds inactivity is on",
-    // this.maxInactivityTimeSeconds);
-    log.info("power down after %s seconds inactivity is on", this.maxInactivityTimeSeconds);
-    addTask("checkInactivity", 5 * 1000, 0, "checkInactivity");
-  }
-
   public void cameraOff() {
     if (opencv != null) {
       opencv.stopCapture();
@@ -629,7 +722,7 @@ public boolean exec(String pythonCode) {
    * This method will try to launch a python command with error handling
    * 
    * @param gesture
-   *                the gesture
+   *          the gesture
    * @return gesture result
    */
   public String execGesture(String gesture) {
@@ -657,7 +750,7 @@ public String execGesture(String gesture) {
    * a filesystem file :P
    * 
    * @param someScriptName
-   *                       execute a resource script
+   *          execute a resource script
    * @return success or failure
    */
   public boolean execScript(String someScriptName) {
@@ -750,16 +843,16 @@ public Long getLastActivityTime() {
       if (leftArm != null)
         lastActivityTime = Math.max(lastActivityTime, leftArm);
       if (rightArm != null)
-      lastActivityTime = Math.max(lastActivityTime, rightArm);
+        lastActivityTime = Math.max(lastActivityTime, rightArm);
       if (leftHand != null)
-      lastActivityTime = Math.max(lastActivityTime, leftHand);
+        lastActivityTime = Math.max(lastActivityTime, leftHand);
       if (rightHand != null)
-      lastActivityTime = Math.max(lastActivityTime, rightHand);
+        lastActivityTime = Math.max(lastActivityTime, rightHand);
       if (torso != null)
-      lastActivityTime = Math.max(lastActivityTime, torso);
+        lastActivityTime = Math.max(lastActivityTime, torso);
     }
 
-      return lastActivityTime;
+    return lastActivityTime;
   }
 
   public InMoov2Arm getLeftArm() {
@@ -859,15 +952,6 @@ public void halfSpeed() {
     sendToPeer("torso", "setSpeed", 20.0, 20.0, 20.0);
   }
 
-  /**
-   * execute python scripts in the init directory on startup of the service
-   * 
-   * @throws IOException
-   */
-  public void loadInitScripts() throws IOException {
-    loadScripts(getResourceDir() + fs + "init");
-  }
-
   public boolean isCameraOn() {
     if (opencv != null) {
       if (opencv.isCapturing()) {
@@ -901,7 +985,7 @@ public void loadGestures() {
    * file should contain 1 method definition that is the same as the filename.
    * 
    * @param directory
-   *                  - the directory that contains the gesture python files.
+   *          - the directory that contains the gesture python files.
    * @return true/false
    */
   public boolean loadGestures(String directory) {
@@ -941,6 +1025,15 @@ public boolean loadGestures(String directory) {
     return true;
   }
 
+  /**
+   * execute python scripts in the init directory on startup of the service
+   * 
+   * @throws IOException
+   */
+  public void loadInitScripts() throws IOException {
+    loadScripts(getResourceDir() + fs + "init");
+  }
+
   /**
    * Generalized directory python script loading method
    * 
@@ -991,8 +1084,7 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D
     moveHand(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1044,10 +1136,8 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring
     moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) {
@@ -1058,10 +1148,8 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin
     moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveTorso(Double topStom, Double midStom, Double lowStom) {
@@ -1090,7 +1178,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
    * comes in from runtime which owns the config list
    * 
    * @param configList
-   *                   list of configs
+   *          list of configs
    */
   public void onConfigList(List<String> configList) {
     this.configList = configList;
@@ -1146,23 +1234,6 @@ public void onJoystickInput(JoystickData input) throws Exception {
     invoke("publishEvent", "joystick");
   }
 
-  public String onNewState(String state) {
-    log.error("onNewState {}", state);
-
-    // put configurable filter here !
-
-    // state substitutions ?
-    // let python subscribe directly to fsm.publishNewState
-
-    // if
-    invoke(state);
-    // depending on configuration ....
-    // call python ?
-    // fire fsm events ?
-    // do defaults ?
-    return state;
-  }
-
   /**
    * Centralized logging system will have all logging from all services,
    * including lower level logs that do not propegate as statuses
@@ -1180,6 +1251,22 @@ public void onLogEvents(List<LogEntry> log) {
     }
   }
 
+  public String onNewState(String state) {
+    log.error("onNewState {}", state);
+
+    // put configurable filter here !
+
+    // state substitutions ?
+    // let python subscribe directly to fsm.publishNewState
+
+    // if
+    invoke(state);
+    // depending on configuration ....
+    // call python ?
+    // fire fsm events ?
+    // do defaults ?
+    return state;
+  }
 
   public OpenCVData onOpenCVData(OpenCVData data) {
     // FIXME - publish event with or without data ? String file reference
@@ -1380,6 +1467,31 @@ public void powerUp() {
     python.execMethod("power_up");
   }
 
+  public void processMessage(String method) {
+    processMessage(method, null);
+  }
+
+  /**
+   * Will publish processing messages to the processor(s) currently subscribed.
+   * 
+   * @param method
+   * @param data
+   */
+  public void processMessage(String method, Object data) {
+    // User processing should not occur until after boot has completed
+    if (!state.equals("boot")) {
+      // FIXME - this needs to be in config
+      // FIXME - change peer name to "processor"
+      // String processor = getPeerName("py4j");
+      String processor = "python";
+
+      Message msg = Message.createMessage(getName(), processor, method, data);
+      // FIXME - is this too much abstraction .. to publish as well as
+      // configurable send ?
+      invoke("publishProcessMessage", msg);
+    }
+  }
+
   /**
    * easy utility to publishMessage
    * 
@@ -1392,12 +1504,6 @@ public void publish(String name, String method, Object... data) {
     invoke("publishMessage", msg);
   }
 
-  public String publishConfigStarted(String configName) {
-    info("config %s started", configName);
-    invoke("publishEvent", "CONFIG STARTED " + configName);
-    return configName;
-  }
-
   public String publishConfigFinished(String configName) {
     info("config %s finished", configName);
     invoke("publishEvent", "CONFIG LOADED " + configName);
@@ -1415,6 +1521,12 @@ public List<String> publishConfigList() {
     return configList;
   }
 
+  public String publishConfigStarted(String configName) {
+    info("config %s started", configName);
+    invoke("publishEvent", "CONFIG STARTED " + configName);
+    return configName;
+  }
+
   /**
    * event publisher for the fsm - although other services potentially can
    * consume and filter this event channel
@@ -1511,31 +1623,6 @@ public Heartbeat publishHeartbeat() {
     return heartbeat;
   }
 
-  public void processMessage(String method) {
-    processMessage(method, null);
-  }
-
-  /**
-   * Will publish processing messages to the processor(s) currently subscribed.
-   * 
-   * @param method
-   * @param data
-   */
-  public void processMessage(String method, Object data) {
-    // User processing should not occur until after boot has completed
-    if (!state.equals("boot")) {
-      // FIXME - this needs to be in config
-      // FIXME - change peer name to "processor"
-      // String processor = getPeerName("py4j");
-      String processor = "python";
-      
-      Message msg = Message.createMessage(getName(), processor, method, data);
-      // FIXME - is this too much abstraction .. to publish as well as
-      // configurable send ?
-      invoke("publishProcessMessage", msg);
-    }
-  }
-
   /**
    * A more extensible interface point than publishEvent FIXME - create
    * interface for this
@@ -1547,8 +1634,7 @@ public Message publishMessage(Message msg) {
     return msg;
   }
 
-  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder,
-      Double omoplate) {
+  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("bicep", bicep);
     map.put("rotate", rotate);
@@ -1562,8 +1648,7 @@ public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double
     return map;
   }
 
-  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure,
-      Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Object> map = new HashMap<>();
     map.put("which", which);
     map.put("thumb", thumb);
@@ -1580,8 +1665,7 @@ public HashMap<String, Object> publishMoveHand(String which, Double thumb, Doubl
     return map;
   }
 
-  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw,
-      Double rollNeck) {
+  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("neck", neck);
     map.put("rothead", rothead);
@@ -1601,8 +1685,7 @@ public HashMap<String, Double> publishMoveLeftArm(Double bicep, Double rotate, D
     return map;
   }
 
-  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1622,8 +1705,7 @@ public HashMap<String, Double> publishMoveRightArm(Double bicep, Double rotate,
     return map;
   }
 
-  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1667,7 +1749,6 @@ public String publishPython(String code) {
     return code;
   }
 
-
   /**
    * publishes a name for NeoPixel.onFlash to consume, in a seperate channel to
    * potentially be used by "speaking only" leds
@@ -1769,8 +1850,7 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     InMoov2Hand hand = getHand(which);
     if (hand == null) {
       warn("%s hand not started", which);
@@ -1780,14 +1860,12 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1803,8 +1881,7 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null);
   }
 
-  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1828,8 +1905,7 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl
   }
 
   @Deprecated
-  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1841,15 +1917,12 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int
     setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   @Override
@@ -1918,15 +1991,49 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In
     setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
+  }
+
+  public boolean setSpeechType(String speechType) {
+
+    if (speechType == null) {
+      error("cannot change speech type to null");
+      return false;
+    }
+
+    if (!speechType.contains(".")) {
+      speechType = "org.myrobotlab.service." + speechType;
+    }
+
+    Runtime runtime = Runtime.getInstance();
+    String peerName = getName() + ".mouth";
+    Plan plan = runtime.getDefault(peerName, speechType);
+    try {
+      SpeechSynthesisConfig mouth = (SpeechSynthesisConfig) plan.get(peerName);
+      mouth.speechRecognizers = new String[] { getName() + ".ear" };
+
+      savePeerConfig("mouth", plan.get(peerName));
+
+      if (isPeerStarted("mouth")) {
+        // restart
+        releasePeer("mouth");
+        startPeer("mouth");
+      }
+
+    } catch (Exception e) {
+      error("could not create config for %s", speechType);
+      return false;
+    }
+
+    return true;
+
+    // updatePeerType("mouth" /* getPeerName("mouth") */, speechType);
+    // return speechType;
   }
 
   public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) {
@@ -1937,6 +2044,11 @@ public void setTorsoSpeed(Integer topStom, Integer midStom, Integer lowStom) {
     setTorsoSpeed((double) topStom, (double) midStom, (double) lowStom);
   }
 
+  // -----------------------------------------------------------------------------
+  // These are methods added that were in InMoov1 that we no longer had in
+  // InMoov2.
+  // From original InMoov1 so we don't loose the
+
   @Deprecated /* use setTorsoSpeed */
   public void setTorsoVelocity(Double topStom, Double midStom, Double lowStom) {
     setTorsoSpeed(topStom, midStom, lowStom);
@@ -1950,11 +2062,6 @@ public void setVoice(String name) {
     }
   }
 
-  // -----------------------------------------------------------------------------
-  // These are methods added that were in InMoov1 that we no longer had in
-  // InMoov2.
-  // From original InMoov1 so we don't loose the
-
   public void sleeping() {
     log.error("sleeping");
   }
@@ -2057,8 +2164,7 @@ public ProgramAB startChatBot() {
       chatBot.setPredicate("null", "");
       // load last user session
       if (!chatBot.getPredicate("name").isEmpty()) {
-        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown")
-            || chatBot.getPredicate("lastUsername").equals("default")) {
+        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown") || chatBot.getPredicate("lastUsername").equals("default")) {
           chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
         }
       }
@@ -2074,8 +2180,7 @@ public ProgramAB startChatBot() {
       // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
       // chatBot.startSession(chatBot.getPredicate("lastUsername"));
       // }
-      if (chatBot.getPredicate("default", "firstinit").isEmpty()
-          || chatBot.getPredicate("default", "firstinit").equals("unknown")
+      if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown")
           || chatBot.getPredicate("default", "firstinit").equals("started")) {
         chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
         invoke("publishEvent", "FIRST INIT");
@@ -2299,138 +2404,4 @@ public void waitTargetPos() {
     sendToPeer("torso", "waitTargetPos");
   }
 
-  public boolean setSpeechType(String speechType) {
-
-    if (speechType == null) {
-      error("cannot change speech type to null");
-      return false;
-    }
-
-    if (!speechType.contains(".")) {
-      speechType = "org.myrobotlab.service." + speechType;
-    }
-
-    Runtime runtime = Runtime.getInstance();
-    String peerName = getName() + ".mouth";
-    Plan plan = runtime.getDefault(peerName, speechType);
-    try {
-      SpeechSynthesisConfig mouth = (SpeechSynthesisConfig) plan.get(peerName);
-      mouth.speechRecognizers = new String[] { getName() + ".ear" };
-
-      savePeerConfig("mouth", plan.get(peerName));
-
-      if (isPeerStarted("mouth")) {
-        // restart
-        releasePeer("mouth");
-        startPeer("mouth");
-      }
-
-    } catch (Exception e) {
-      error("could not create config for %s", speechType);
-      return false;
-    }
-
-    return true;
-
-    // updatePeerType("mouth" /* getPeerName("mouth") */, speechType);
-    // return speechType;
-  }
-
-  public static void main(String[] args) {
-    try {
-
-      LoggingFactory.init(Level.ERROR);
-      // Platform.setVirtual(true);
-      // Runtime.start("s01", "Servo");
-      // Runtime.start("intro", "Intro");
-
-      Runtime.startConfig("dev");
-
-      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
-      // webgui.setSsl(true);
-      webgui.autoStartBrowser(false);
-      // webgui.setPort(8888);
-      webgui.startService();
-      InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-
-      boolean done = true;
-      if (done) {
-        return;
-      }
-
-      OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {
-      });
-      ocvConfig.flip = true;
-      i01.setPeerConfigValue("opencv", "flip", true);
-      // i01.savePeerConfig("", null);
-
-      // Runtime.startConfig("default");
-
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
-      // "intro", "Intro", "python", "Python" });
-
-      Runtime.start("python", "Python");
-      // Runtime.start("ros", "Ros");
-      Runtime.start("intro", "Intro");
-      // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-      // i01.startPeer("simulator");
-      // Runtime.startConfig("i01-05");
-      // Runtime.startConfig("pir-01");
-
-      // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly");
-      // i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-
-      // polly.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-      // i01.startPeer("mouth");
-      // i01.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-
-      Runtime.start("python", "Python");
-
-      // i01.startSimulator();
-      Plan plan = Runtime.load("webgui", "WebGui");
-      // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui");
-      // webgui.autoStartBrowser = false;
-      Runtime.startConfig("webgui");
-      Runtime.start("webgui", "WebGui");
-
-      Random random = (Random) Runtime.start("random", "Random");
-
-      random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-      random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-
-      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          130.0, 175.0);
-      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          5.0, 40.0);
-
-      random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
-      random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
-
-      random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0);
-      random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0);
-
-      random.save();
-
-      // i01.startChatBot();
-      //
-      // i01.startAll("COM3", "COM4");
-      Runtime.start("python", "Python");
-
-    } catch (Exception e) {
-      log.error("main threw", e);
-    }
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 014cfcf0e9..c599a5a1ad 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -519,11 +519,10 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer")));
     listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel")));
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
-    // listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
+    // listeners.add(new Listener("publishProcessMessage",
+    // getPeerName("python"), "onPythonMessage"));
     listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
     listeners.add(new Listener("publishPython", "python"));
-    
-    
 
     // InMoov2 --to--> InMoov2
     listeners.add(new Listener("publishMoveHead", name));
@@ -536,12 +535,11 @@ public Plan getDefault(Plan plan, String name) {
     // service --to--> InMoov2
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
-    
+
     OakDConfig oakd = (OakDConfig) plan.get(getPeerName("oakd"));
     oakd.listeners.add(new Listener("publishClassification", name));
     oakd.getPeer("py4j").name = getPeerName("py4j");
 
-    
     webxr.listeners.add(new Listener("publishJointAngles", name));
 
     // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name));

From 683808410395130c743fc102628b34999b6c7803 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 06:58:19 -0800
Subject: [PATCH 006/131] recovering from bad merge

---
 .../java/org/myrobotlab/service/InMoov2.java  | 122 ++++++------------
 1 file changed, 41 insertions(+), 81 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 568edece8d..caa06027f3 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -55,8 +55,7 @@
 import org.slf4j.Logger;
 
 public class InMoov2 extends Service<InMoov2Config>
-    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider,
-    IKJointAngleListener {
+    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
 
   public class Heart implements Runnable {
     private final ReentrantLock lock = new ReentrantLock();
@@ -129,7 +128,7 @@ public Heartbeat(InMoov2 inmoov) {
    * This method will load a python file into the python interpreter.
    * 
    * @param file
-   *             file to load
+   *          file to load
    * @return success/failure
    */
   @Deprecated /* use execScript - this doesn't handle resources correctly */
@@ -235,15 +234,11 @@ public static void main(String[] args) {
       random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
       random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
 
-      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
 
-      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          130.0, 175.0);
-      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          5.0, 40.0);
+      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 130.0, 175.0);
+      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 5.0, 40.0);
 
       random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
       random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
@@ -263,8 +258,6 @@ public static void main(String[] args) {
     }
   }
 
-  <<<<<<<HEAD
-
   protected Double batteryLevel = 100.0;
 
   /**
@@ -272,7 +265,6 @@ public static void main(String[] args) {
    */
   protected int bootCount = 0;
 
-  =======>>>>>>>60e56597d 29d 490d 553855 aff73d5930130c063d
   protected transient ProgramAB chatBot;
 
   protected List<String> configList;
@@ -352,8 +344,7 @@ public static void main(String[] args) {
 
   public InMoov2(String n, String id) {
     super(n, id);
-    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
-        "pt-PT", "tr-TR");
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -731,7 +722,7 @@ public boolean exec(String pythonCode) {
    * This method will try to launch a python command with error handling
    * 
    * @param gesture
-   *                the gesture
+   *          the gesture
    * @return gesture result
    */
   public String execGesture(String gesture) {
@@ -759,7 +750,7 @@ public String execGesture(String gesture) {
    * a filesystem file :P
    * 
    * @param someScriptName
-   *                       execute a resource script
+   *          execute a resource script
    * @return success or failure
    */
   public boolean execScript(String someScriptName) {
@@ -837,18 +828,11 @@ public InMoov2Head getHead() {
    */
   public Long getLastActivityTime() {
     Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
-    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime()
-        : null;
-    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime()
-        : null;
-    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null
-        ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime()
-        : null;
-    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null
-        ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime()
-        : null;
-    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime()
-        : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
 
     Long lastActivityTime = null;
 
@@ -1001,7 +985,7 @@ public void loadGestures() {
    * file should contain 1 method definition that is the same as the filename.
    * 
    * @param directory
-   *                  - the directory that contains the gesture python files.
+   *          - the directory that contains the gesture python files.
    * @return true/false
    */
   public boolean loadGestures(String directory) {
@@ -1100,8 +1084,7 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D
     moveHand(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1153,10 +1136,8 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring
     moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) {
@@ -1167,10 +1148,8 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin
     moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveTorso(Double topStom, Double midStom, Double lowStom) {
@@ -1199,7 +1178,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
    * comes in from runtime which owns the config list
    * 
    * @param configList
-   *                   list of configs
+   *          list of configs
    */
   public void onConfigList(List<String> configList) {
     this.configList = configList;
@@ -1260,7 +1239,7 @@ public void onJoystickInput(JoystickData input) throws Exception {
    * including lower level logs that do not propegate as statuses
    * 
    * @param log
-   *            - flushed log from Log service
+   *          - flushed log from Log service
    */
   public void onLogEvents(List<LogEntry> log) {
     // scan for warn or errors
@@ -1603,8 +1582,7 @@ public Heartbeat publishHeartbeat() {
       }
 
       // interval event firing
-      if (config.stateRandomInterval != null
-          && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
         // fsm.fire("random");
         stateLastRandomTime = System.currentTimeMillis();
       }
@@ -1656,8 +1634,7 @@ public Message publishMessage(Message msg) {
     return msg;
   }
 
-  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder,
-      Double omoplate) {
+  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("bicep", bicep);
     map.put("rotate", rotate);
@@ -1671,8 +1648,7 @@ public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double
     return map;
   }
 
-  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure,
-      Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Object> map = new HashMap<>();
     map.put("which", which);
     map.put("thumb", thumb);
@@ -1689,8 +1665,7 @@ public HashMap<String, Object> publishMoveHand(String which, Double thumb, Doubl
     return map;
   }
 
-  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw,
-      Double rollNeck) {
+  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("neck", neck);
     map.put("rothead", rothead);
@@ -1710,8 +1685,7 @@ public HashMap<String, Double> publishMoveLeftArm(Double bicep, Double rotate, D
     return map;
   }
 
-  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1731,8 +1705,7 @@ public HashMap<String, Double> publishMoveRightArm(Double bicep, Double rotate,
     return map;
   }
 
-  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1877,8 +1850,7 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     InMoov2Hand hand = getHand(which);
     if (hand == null) {
       warn("%s hand not started", which);
@@ -1888,14 +1860,12 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1911,8 +1881,7 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null);
   }
 
-  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1936,8 +1905,7 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl
   }
 
   @Deprecated
-  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1949,15 +1917,12 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int
     setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   @Override
@@ -2026,15 +1991,12 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In
     setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public boolean setSpeechType(String speechType) {
@@ -2202,8 +2164,7 @@ public ProgramAB startChatBot() {
       chatBot.setPredicate("null", "");
       // load last user session
       if (!chatBot.getPredicate("name").isEmpty()) {
-        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown")
-            || chatBot.getPredicate("lastUsername").equals("default")) {
+        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown") || chatBot.getPredicate("lastUsername").equals("default")) {
           chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
         }
       }
@@ -2219,8 +2180,7 @@ public ProgramAB startChatBot() {
       // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
       // chatBot.startSession(chatBot.getPredicate("lastUsername"));
       // }
-      if (chatBot.getPredicate("default", "firstinit").isEmpty()
-          || chatBot.getPredicate("default", "firstinit").equals("unknown")
+      if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown")
           || chatBot.getPredicate("default", "firstinit").equals("started")) {
         chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
         invoke("publishEvent", "FIRST INIT");

From b5794f5f33c46b96be1f2797fd95a13045219f8a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 07:01:45 -0800
Subject: [PATCH 007/131] reset randomtest

---
 .../org/myrobotlab/service/RandomTest.java    | 55 +++++++++----------
 1 file changed, 25 insertions(+), 30 deletions(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 1e42ab9e3a..9c3739f510 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -17,23 +17,24 @@ public class RandomTest extends AbstractServiceTest {
              * rarely happens - seems not useful and silly
              */
   public Service createService() throws Exception {
-    return (Service) Runtime.start("random", "Random");
+    return (Service) Runtime.start("randomTest", "Random");
   }
-
+  
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-    // clean our config directory
-    Runtime.removeConfig("RandomTest");
+      // clean our config directory
+    // Runtime.removeConfig("RandomTest");
     // set our config
     Runtime.setConfig("RandomTest");
   }
+  
 
   @Override
   public void testService() throws Exception {
     Clock clock = (Clock) Runtime.start("clock", "Clock");
-    Random random = (Random) Runtime.start("random", "Random");
+    Random random = (Random) Runtime.start("randomTest", "Random");
 
     clock.stopClock();
     clock.setInterval(1000);
@@ -42,71 +43,65 @@ public void testService() throws Exception {
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.enable();
 
-    sleep(500);
+    sleep(1000);
 
     assertTrue("should have method", random.getKeySet().contains("clock.setInterval"));
-
-    assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()),
-        5000 <= clock.getInterval());
-    assertTrue(String.format("random method 1 should be %d <= 10000 values", clock.getInterval()),
-        clock.getInterval() <= 10000);
-
+    
+    assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
+    assertTrue(String.format("random method 1 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    
     random.remove("clock.setInterval");
-
+    
     assertTrue("should not have method", !random.getKeySet().contains("clock.setInterval"));
 
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.addRandom(0, 200, "clock", "startClock");
-
+    
     sleep(500);
     assertTrue("clock should be started 1", clock.isClockRunning());
-
+    
     // disable all of a services random events
     random.disable("clock.startClock");
     clock.stopClock();
     sleep(250);
     assertTrue("clock should not be started 1", !clock.isClockRunning());
-
+    
     // enable all of a service's random events
     random.enable("clock.startClock");
     sleep(250);
     assertTrue("clock should be started 2", clock.isClockRunning());
-
+    
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
     clock.setInterval(999999);
     sleep(200);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
-    assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()),
-        5000 <= clock.getInterval());
-    assertTrue(String.format("random method 2 should be %d <= 10000 values", clock.getInterval()),
-        clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
+    assertTrue(String.format("random method 2 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
 
     // disable all
     random.disable();
     sleep(200);
     clock.setInterval(999999);
-    assertTrue("clock should not be started 4", !clock.isClockRunning());
-    assertEquals(999999, (long) clock.getInterval());
+    assertTrue("clock should not be started 4", !clock.isClockRunning());   
+    assertEquals(999999, (long)clock.getInterval());
 
     // re-enable all that were previously enabled but not explicitly disabled ones
     random.enable();
     sleep(1000);
     assertTrue("clock should not be started 5", !clock.isClockRunning());
-    assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()),
-        5000 <= clock.getInterval());
-    assertTrue(String.format("random method 3 should be %d <= 10000 values", clock.getInterval()),
-        clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
+    assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
 
     clock.stopClock();
     random.purge();
-
+        
     Map<String, RandomMessage> events = random.getRandomEvents();
     assertTrue(events.size() == 0);
-
+    
     random.addRandom("named task", 200, 500, "clock", "setInterval", 100, 1000, 10);
-
+    
     clock.releaseService();
     random.releaseService();
 

From 37a379590f162c92a62358c35c5605aaf75d98c7 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 08:13:15 -0800
Subject: [PATCH 008/131] finite state history

---
 .../service/views/FiniteStateMachineGui.html    | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html b/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
index 067ee7624c..b61f5af303 100644
--- a/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
@@ -54,5 +54,22 @@ <h3>Last Event {{event}} Current State: {{current}}</h3>
                 <td></td>
             </tr>
         </table>
+<table class="table-condensed table-striped table-bordered">
+  <thead>
+    <tr>
+      <th>Timestamp</th>
+      <th>State</th>
+      <th>Event</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="item in service.history">
+      <td>{{ item.ts }}</td>
+      <td>{{ item.state }}</td>
+      <td>{{ item.event }}</td>
+    </tr>
+  </tbody>
+</table>            
+            
     </div>
 </div>

From a7c1b662f74b9cbd73101ee3faff739cc6f80ff9 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 13:56:02 -0800
Subject: [PATCH 009/131] fixed fire fsm event

---
 .../java/org/myrobotlab/service/InMoov2.java  | 23 +++++++++++--------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index caa06027f3..719025ce8c 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -778,9 +778,15 @@ public void finishedGesture(String nameOfGesture) {
     }
   }
 
-  // FIXME - this isn't the callback for fsm - why is it needed here ?
+  /**
+   * Fire an event to the FSM, potentially this can cause a state change
+   * 
+   * @param event
+   */
   public void fire(String event) {
-    invoke("publishEvent", event);
+    // Should this be sent to chatbot too ?
+    // invoke("publishEvent", event);
+    fsm.fire(event);
   }
 
   public void fullSpeed() {
@@ -2283,16 +2289,13 @@ public ServiceInterface startPeer(String peer) {
   @Override
   public void startService() {
     super.startService();
-    // FIXME - hardcoded peer no choice of type
+
+    // This is required the core of InMoov is
+    // a FSM ProgramAB and some form of Python/Jython
     fsm = (FiniteStateMachine) startPeer("fsm");
 
-    // a python processor is
-    // necessary for InMoov2 to properly
-    // function but this is not the place to start it
-    // it should be a peer definition too, it can be "python"
-    // it doesn't need to be i01.python to be a peer
-    // also should determine type Py4j or Python
-    // Runtime.start("python");
+    // A python process is required - should be defined as a peer
+    // of Type Python or Py4j
 
     // just for comparing config with current "default"
     // debugging only

From b90bd80573f4266edb384bf282505bed424eb0f9 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 21 Jan 2024 14:19:19 -0800
Subject: [PATCH 010/131] requested updates

---
 .../org/myrobotlab/programab/models/Oob.java  | 12 +++--
 .../myrobotlab/programab/models/Sraix.java    | 16 +++++-
 .../myrobotlab/programab/models/Template.java | 45 ++++++----------
 .../myrobotlab/programab/TemplateTest.java    | 52 +++++++++++++++++++
 4 files changed, 91 insertions(+), 34 deletions(-)
 create mode 100644 src/test/java/org/myrobotlab/programab/TemplateTest.java

diff --git a/src/main/java/org/myrobotlab/programab/models/Oob.java b/src/main/java/org/myrobotlab/programab/models/Oob.java
index 833bab5a0f..5e0d99c4cf 100644
--- a/src/main/java/org/myrobotlab/programab/models/Oob.java
+++ b/src/main/java/org/myrobotlab/programab/models/Oob.java
@@ -4,11 +4,17 @@
 
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 
+/**
+ * AIML 2.0 Oob Out Of Band xml defined with mrl - legacy and mrljson - json
+ * typed message
+ * 
+ * @author GroG
+ *
+ */
 public class Oob {
-  
+
   public String mrljson;
-  
+
   @JacksonXmlElementWrapper(useWrapping = false)
   public List<Mrl> mrl;
 }
-
diff --git a/src/main/java/org/myrobotlab/programab/models/Sraix.java b/src/main/java/org/myrobotlab/programab/models/Sraix.java
index 99b0639cb6..1b130ad805 100644
--- a/src/main/java/org/myrobotlab/programab/models/Sraix.java
+++ b/src/main/java/org/myrobotlab/programab/models/Sraix.java
@@ -1,10 +1,22 @@
 package org.myrobotlab.programab.models;
 
-// FIXME add attributes and internal tags
+/**
+ * Basic Sraix model, AIML 2.0 has more elements but these seemed like the most
+ * relevant and ar actually used.
+ * 
+ * @author GroG
+ *
+ */
 public class Sraix {
 
+  /**
+   * Search text when a query is sent to a remote system
+   */
   public String search;
 
+  /**
+   * Oob is Out Of Band text which can be handled by internal processing
+   */
   public Oob oob;
-  
+
 }
diff --git a/src/main/java/org/myrobotlab/programab/models/Template.java b/src/main/java/org/myrobotlab/programab/models/Template.java
index 91f8e5de51..d657973005 100644
--- a/src/main/java/org/myrobotlab/programab/models/Template.java
+++ b/src/main/java/org/myrobotlab/programab/models/Template.java
@@ -5,47 +5,34 @@
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
 
-//@JacksonXmlRootElement(localName = "template")
-//@JsonIgnoreProperties(ignoreUnknown = true)
+/**
+ * General aiml template used for future parsing
+ * 
+ * @author GroG
+ */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class Template {
-  // @JacksonXmlElementWrapper(useWrapping = false)
-  
+
   @JacksonXmlProperty(localName = "template")
-  
   @JacksonXmlText
-  public String text;  
-  
-  
-public Oob oob;
-  
-//  @JsonProperty("ignorable")
-//  public List<Oob> oob;
-//
-//  public List<Oob> getOob() {
-//    return oob;
-//  }
-//
-//  public void setOob(List<Oob> oob) {
-//    this.oob = oob;
-//  }
-  
+  public String text;
+
+  public Oob oob;
+
   public static void main(String[] args) {
 
     try {
-      
-      // String xml = "<template>XXX<oob><mrl><service>blah</service><method>method</method></mrl></oob></template>";
-      // String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method></mrl><mrl><service>blah2</service><method>method2</method></mrl></oob></template>";
+
       String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method><param>p1</param><param>p2</param><param>p3</param></mrl><mrl><service>blah2</service><method>method2</method></mrl><mrljson>[\"method\":\"doIt\",\"data\":[\"p1\"]]</mrljson></oob></template>";
-      
+
       XmlMapper xmlMapper = new XmlMapper();
       Template template = xmlMapper.readValue(xml, Template.class);
-      
+
       System.out.println(template);
-      
-    } catch(Exception e) {
+
+    } catch (Exception e) {
       e.printStackTrace();
     }
-    }
+  }
 
 }
diff --git a/src/test/java/org/myrobotlab/programab/TemplateTest.java b/src/test/java/org/myrobotlab/programab/TemplateTest.java
new file mode 100644
index 0000000000..8d2beb245d
--- /dev/null
+++ b/src/test/java/org/myrobotlab/programab/TemplateTest.java
@@ -0,0 +1,52 @@
+package org.myrobotlab.programab;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.programab.models.Mrl;
+import org.myrobotlab.programab.models.Template;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+public class TemplateTest {
+
+  public final static Logger log = LoggerFactory.getLogger(TemplateTest.class);
+
+  @Test
+  public void testXmlParsing() {
+    try {
+
+      String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method><param>p1</param><param>p2</param><param>p3</param></mrl><mrl><service>blah2</service><method>method2</method></mrl><mrljson>[\"method\":\"doIt\",\"data\":[\"p1\"]]</mrljson></oob></template>";
+
+      XmlMapper xmlMapper = new XmlMapper();
+      Template template = xmlMapper.readValue(xml, Template.class);
+
+      assertNotNull(template);
+      assertEquals("XXXX", template.text);
+
+      // Verify Oob parsing
+      assertNotNull(template.oob);
+      assertEquals(2, template.oob.mrl.size());
+
+      // Verify the first Mrl
+      Mrl mrl1 = template.oob.mrl.get(0);
+      assertEquals("blah1", mrl1.service);
+      assertEquals("method1", mrl1.method);
+      assertEquals(3, mrl1.params.size());
+
+      // Verify the second Mrl
+      Mrl mrl2 = template.oob.mrl.get(1);
+      assertEquals("blah2", mrl2.service);
+      assertEquals("method2", mrl2.method);
+      assertNull(mrl2.params);
+
+    } catch (Exception e) {
+      fail("Exception occurred: " + e.getMessage());
+    }
+  }
+}

From 8d4fdb992b9bc75e81993e48fb87f73f8644fa65 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 23 Jan 2024 22:34:44 -0800
Subject: [PATCH 011/131] init scripts turned off

---
 src/main/java/org/myrobotlab/service/Python.java          | 2 +-
 .../java/org/myrobotlab/service/config/InMoov2Config.java | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Python.java b/src/main/java/org/myrobotlab/service/Python.java
index c17a905f23..44f38f4bac 100644
--- a/src/main/java/org/myrobotlab/service/Python.java
+++ b/src/main/java/org/myrobotlab/service/Python.java
@@ -677,7 +677,7 @@ public void onStarted(String serviceName) {
 
   @Override
   public void onReleased(String serviceName) {
-    String registerScript = String.format("%s = None\n", CodecUtils.getSafeReferenceName(serviceName));
+    String registerScript = String.format("%s = None\n", CodecUtils.getSafeReferenceName(CodecUtils.getShortName(serviceName)));
     exec(registerScript, false);
   }
 
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 1a4cbf51fe..0a4937210a 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -68,17 +68,17 @@ public class InMoov2Config extends ServiceConfig {
    */
   public long heartbeatInterval = 3000;
 
-  public boolean loadAppsScripts = true;
+  public boolean loadAppsScripts = false;
 
   /**
    * loads all python gesture files in the gesture directory
    */
-  public boolean loadGestures = true;
+  public boolean loadGestures = false;
 
   /**
    * executes all scripts in the init directory on startup
    */
-  public boolean loadInitScripts = true;
+  public boolean loadInitScripts = false;
 
   /**
    * default to null - allow the OS to set it, unless explicilty set
@@ -546,7 +546,7 @@ public Plan getDefault(Plan plan, String name) {
     // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
 
     // Needs upcoming pr
-    // fsm.listeners.add(new Listener("publishStateChange", name));
+    fsm.listeners.add(new Listener("publishStateChange", name));
 
     return plan;
   }

From 9aacb0b81e6401e0ebfc0444f8994d76cd19a800 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 26 Jan 2024 08:35:09 -0800
Subject: [PATCH 012/131] publishStateChange vs onStateChange

---
 .../org/myrobotlab/service/HtmlFilter.java    | 28 ++++-----
 .../java/org/myrobotlab/service/InMoov2.java  | 27 ++++----
 .../org/myrobotlab/service/ProgramAB.java     | 62 +++++++++++++------
 .../service/config/InMoov2Config.java         |  5 +-
 .../app/service/js/FiniteStateMachineGui.js   |  1 +
 5 files changed, 72 insertions(+), 51 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/HtmlFilter.java b/src/main/java/org/myrobotlab/service/HtmlFilter.java
index ff0a4b4fb1..66134acd34 100644
--- a/src/main/java/org/myrobotlab/service/HtmlFilter.java
+++ b/src/main/java/org/myrobotlab/service/HtmlFilter.java
@@ -36,8 +36,7 @@ public HtmlFilter(String n, String id) {
 
   // helper function to add html tags
   public String addHtml(String text) {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    return c.preHtmlTag + text + c.postHtmlTag;
+    return config.preHtmlTag + text + config.postHtmlTag;
   }
 
   public void addTextListener(TextListener service) {
@@ -45,18 +44,15 @@ public void addTextListener(TextListener service) {
   }
 
   public String getPostHtmlTag() {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    return c.postHtmlTag;
+    return config.postHtmlTag;
   }
 
   public String getPreHtmlTag() {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    return c.preHtmlTag;
+    return config.preHtmlTag;
   }
 
   public boolean isStripHtml() {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    return c.stripHtml;
+    return config.stripHtml;
   }
 
   @Override
@@ -67,20 +63,20 @@ public void onText(String text) {
 
   @Override
   public String processText(String text) {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
+    
 
     invoke("publishRawText", text);
 
     String processedText = text;
 
-    if (c.stripHtml) {
+    if (config.stripHtml) {
       // clean text
       processedText = stripHtml(text);
     } else {
       processedText = addHtml(text);
     }
 
-    if (c.stripUrls) {
+    if (config.stripUrls) {
       processedText = stripUrls(processedText);
     }
 
@@ -104,8 +100,8 @@ public String publishText(String text) {
    *          - a string to append to the text
    */
   public void setPostHtmlTag(String postHtmlTag) {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    c.postHtmlTag = postHtmlTag;
+    
+    config.postHtmlTag = postHtmlTag;
   }
 
   /**
@@ -115,8 +111,7 @@ public void setPostHtmlTag(String postHtmlTag) {
    *          - a string to prepend to the text.
    */
   public void setPreHtmlTag(String preHtmlTag) {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    c.preHtmlTag = preHtmlTag;
+    config.preHtmlTag = preHtmlTag;
   }
 
   /**
@@ -127,8 +122,7 @@ public void setPreHtmlTag(String preHtmlTag) {
    *          - if true, all content between &lt;and &gt; will be removed.
    */
   public void setStripHtml(boolean stripHtml) {
-    HtmlFilterConfig c = (HtmlFilterConfig) config;
-    c.stripHtml = stripHtml;
+    config.stripHtml = stripHtml;
   }
 
   // helper function to strip html tags.
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 719025ce8c..ab6bb08b94 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -34,6 +34,7 @@
 import org.myrobotlab.opencv.OpenCVData;
 import org.myrobotlab.programab.PredicateEvent;
 import org.myrobotlab.programab.Response;
+import org.myrobotlab.service.FiniteStateMachine.StateChange;
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
@@ -1388,6 +1389,8 @@ public void onStartSpeaking(String utterance) {
   }
 
   /**
+   * publishStateChange
+   * 
    * The integration between the FiniteStateMachine (fsm) and the InMoov2
    * service and potentially other services (Python, ProgramAB) happens here.
    * 
@@ -1405,22 +1408,19 @@ public void onStartSpeaking(String utterance) {
    * 
    * Depending on config:
    * 
-   * 
    * @param stateChange
    * @return
    */
-  public FiniteStateMachine.StateChange onStateChange(FiniteStateMachine.StateChange stateChange) {
-    try {
-      log.info("onStateChange {}", stateChange);
-
-      lastState = state;
-      state = stateChange.state;
+  public StateChange publishStateChange(StateChange stateChange) {
+    log.info("publishStateChange {}", stateChange);
+    
+    log.info("onStateChange {}", stateChange);
 
-      processMessage("onStateChange", stateChange);
+    lastState = state;
+    state = stateChange.state;
 
-    } catch (Exception e) {
-      error(e);
-    }
+    processMessage("onStateChange", stateChange);
+    
     return stateChange;
   }
 
@@ -1583,7 +1583,8 @@ public Heartbeat publishHeartbeat() {
       }
 
       if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) {
-        fsm.fire("idle");
+        // idle event to be handled with the processor
+        processMessage("onIdle");
         stateLastIdleTime = System.currentTimeMillis();
       }
 
@@ -1628,7 +1629,7 @@ public Heartbeat publishHeartbeat() {
     processMessage("onHeartbeat", heartbeat);
     return heartbeat;
   }
-
+  
   /**
    * A more extensible interface point than publishEvent FIXME - create
    * interface for this
diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java
index 332d546df9..804115b67e 100644
--- a/src/main/java/org/myrobotlab/service/ProgramAB.java
+++ b/src/main/java/org/myrobotlab/service/ProgramAB.java
@@ -97,7 +97,7 @@ public class ProgramAB extends Service<ProgramABConfig>
   boolean peerSearch = true;
 
   transient SimpleLogPublisher logPublisher = null;
-  
+
   final transient private OobProcessor oobProcessor;
 
   /**
@@ -762,7 +762,7 @@ public void addCategory(String pattern, String template, String that) {
   public void addCategory(String pattern, String template) {
     addCategory(pattern, template, "*");
   }
-  
+
   /**
    * Verifies and adds a new path to the search directories for bots
    * 
@@ -1103,7 +1103,7 @@ public ProgramABConfig apply(ProgramABConfig c) {
         addBotPath(botPath);
       }
     }
-    
+
     if (c.botDir == null) {
       c.botDir = getResourceDir();
     }
@@ -1116,19 +1116,40 @@ public ProgramABConfig apply(ProgramABConfig c) {
     if (c.currentUserName != null) {
       setCurrentUserName(c.currentUserName);
     }
-    
+
     if (c.currentBotName != null) {
       setCurrentBotName(c.currentBotName);
-    }    
-    
+    }
+
     if (c.startTopic != null) {
-      setTopic(c.startTopic);  
+      setTopic(c.startTopic);
     }
-    
 
     return c;
   }
 
+  /**
+   * Set the current locale for this service. In ProgramAB's case if a bot
+   * matches the local then set the bot
+   * 
+   */
+  @Override
+  public void setLocale(String code) {
+    if (code == null) {
+      error("locale cannot be null");
+      return;
+    }
+    locale = new Locale(code);
+    log.info("{} new locale is {}", getName(), code);
+
+    for (String bot : bots.keySet()) {
+      if (code.equals(bot)) {
+        setCurrentBotName(bot);
+      }
+    }
+    broadcastState();
+  }
+
   public static void main(String args[]) {
     try {
       LoggingFactory.init("INFO");
@@ -1306,7 +1327,7 @@ public void sleep() {
 
   @Override
   public void onUtterance(Utterance utterance) throws Exception {
-    
+
     log.info("Utterance Received " + utterance);
 
     boolean talkToBots = false;
@@ -1371,17 +1392,18 @@ public void onUtterance(Utterance utterance) throws Exception {
       }
     }
   }
-  
+
   /**
    * This receiver can take a config published by another service and sync
    * predicates from it
+   * 
    * @param cfg
    */
   public void onConfig(ServiceConfig cfg) {
-    Yaml yaml = new Yaml();    
+    Yaml yaml = new Yaml();
     String yml = yaml.dumpAsMap(cfg);
     Map<String, Object> cfgMap = yaml.load(yml);
-    
+
     for (Map.Entry<String, Object> entry : cfgMap.entrySet()) {
       if (entry.getValue() == null) {
         setPredicate("cfg_" + entry.getKey(), null);
@@ -1389,7 +1411,7 @@ public void onConfig(ServiceConfig cfg) {
         setPredicate("cfg_" + entry.getKey(), entry.getValue().toString());
       }
     }
-    
+
     invoke("getPredicates");
   }
 
@@ -1402,19 +1424,19 @@ public TopicChange publishTopic(TopicChange topicChange) {
     return topicChange;
   }
 
-  public String getTopic() {    
+  public String getTopic() {
     return getPredicate(getCurrentUserName(), "topic");
   }
-  
-  public String getTopic(String username) {    
+
+  public String getTopic(String username) {
     return getPredicate(username, "topic");
   }
-  
-  public void setTopic(String username, String topic) {    
+
+  public void setTopic(String username, String topic) {
     setPredicate(username, "topic", topic);
   }
-  
-  public void setTopic(String topic) {    
+
+  public void setTopic(String topic) {
     setPredicate(getCurrentUserName(), "topic", topic);
   }
 
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 0a4937210a..1ec4694f0e 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -522,6 +522,7 @@ public Plan getDefault(Plan plan, String name) {
     // listeners.add(new Listener("publishProcessMessage",
     // getPeerName("python"), "onPythonMessage"));
     listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
+    
     listeners.add(new Listener("publishPython", "python"));
 
     // InMoov2 --to--> InMoov2
@@ -535,6 +536,8 @@ public Plan getDefault(Plan plan, String name) {
     // service --to--> InMoov2
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
+    
+    htmlFilter.listeners.add(new Listener("publishText", name));
 
     OakDConfig oakd = (OakDConfig) plan.get(getPeerName("oakd"));
     oakd.listeners.add(new Listener("publishClassification", name));
@@ -546,7 +549,7 @@ public Plan getDefault(Plan plan, String name) {
     // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
 
     // Needs upcoming pr
-    fsm.listeners.add(new Listener("publishStateChange", name));
+    fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
 
     return plan;
   }
diff --git a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
index 42c559c999..d08f8f2fc8 100644
--- a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
@@ -51,6 +51,7 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta
             break
         case 'onStateChange':
             $scope.current = data.current
+            $scope.service.history.push(data)
             $scope.$apply()
             break
         default:

From 4c9e5e0fd976f4d349bdd1b7d5f3338e20a79532 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 26 Jan 2024 15:04:48 -0800
Subject: [PATCH 013/131] execScript()

---
 .../java/org/myrobotlab/service/InMoov2.java  | 22 ++++++++++---------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index ab6bb08b94..146b72e68d 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -468,8 +468,7 @@ synchronized public void boot() {
        */
 
       // load the InMoov2.py and publish it for Python/Jython or Py4j to consume
-      String script = getResourceAsString("InMoov2.py");
-      invoke("publishPython", script);
+      execScript();
 
       // TODO - MAKE BOOT REPORT !!!! deliver it on a heartbeat
       runtime.invoke("publishConfigList");
@@ -740,6 +739,14 @@ public String execGesture(String gesture) {
     }
     return python.evalAndWait(gesture);
   }
+  
+  /**
+   * Reload the InMoov2.py script
+   */
+  public void execScript() {
+    execScript("InMoov2.py");
+  }
+
 
   /**
    * FIXME - I think there was lots of confusion of executing resources or just
@@ -754,15 +761,9 @@ public String execGesture(String gesture) {
    *          execute a resource script
    * @return success or failure
    */
-  public boolean execScript(String someScriptName) {
-    try {
-      Python p = (Python) Runtime.start("python", "Python");
+  public void execScript(String someScriptName) {
       String script = getResourceAsString(someScriptName);
-      return p.exec(script, true);
-    } catch (Exception e) {
-      error("unable to execute script %s", someScriptName);
-      return false;
-    }
+      invoke("publishPython", script);
   }
 
   public void finishedGesture() {
@@ -1801,6 +1802,7 @@ public void releasePeer(String peerKey) {
       invoke("publishEvent", "STOPPED " + peerKey);
     }
   }
+  
 
   @Override
   public void releaseService() {

From 5bbdfe0e997a73b89ecb448e90820149da64baee Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 28 Jan 2024 09:54:22 -0800
Subject: [PATCH 014/131] config utils

---
 .../org/myrobotlab/config/ConfigUtils.java    | 71 +++++++++++++++++++
 .../org/myrobotlab/framework/Service.java     | 16 ++---
 src/main/java/org/myrobotlab/io/FileIO.java   | 36 +++-------
 .../java/org/myrobotlab/service/InMoov2.java  | 14 +++-
 .../java/org/myrobotlab/service/Runtime.java  | 14 +++-
 .../service/config/InMoov2Config.java         |  2 -
 .../java/org/myrobotlab/io/FileIOTest.java    |  5 --
 7 files changed, 113 insertions(+), 45 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/config/ConfigUtils.java

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
new file mode 100644
index 0000000000..35c8a776a8
--- /dev/null
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -0,0 +1,71 @@
+package org.myrobotlab.config;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.io.FileIO;
+import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.RuntimeConfig;
+
+public class ConfigUtils {
+
+  /**
+   * This gets the current resource root without starting a Runtime instance if
+   * not already started. The resource root depends on config, if Runtime is
+   * running the logic and current config name is already available. If Runtime
+   * is not running, we need to go through a series of steps to deterime where
+   * the resource root is configured.
+   * 
+   * @return
+   */
+  public static String getResourceRoot() {
+
+    String resource = "resource";
+
+    // check if runtime is running
+    if (!Runtime.isAvailable()) {
+      // check for start.yml
+
+      File checkStartYml = new File("start.yml");
+      StartYml startYml = new StartYml();
+      if (checkStartYml.exists()) {
+        String yml;
+        try {
+          yml = FileIO.toString("start.yml");
+          startYml = CodecUtils.fromYaml(yml, StartYml.class);
+
+          // see if autostart is on with a config
+          if (startYml.enable) {
+            // use that config to find runtime.yml
+
+            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + startYml.config + File.separator + "runtime.yml");
+            if (runtimeYml.exists()) {
+              // parse that file look for resource: entry in file
+              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+              resource = config.resource;
+            }
+
+          } else {
+            // start.yml enable = false / so we'll use default config
+            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + "default" + File.separator + "runtime.yml");
+            if (runtimeYml.exists()) {
+              // parse that file look for resource: entry in file
+              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+              resource = config.resource;
+            }
+          }
+
+        } catch (IOException e) {
+          // problem getting or parsing
+          // going to assume default "resource"
+        }
+      } // no startYml
+      return resource;
+    } else {
+      // Runtime is available - ask it
+      return Runtime.getInstance().getConfig().resource;
+    }
+  }
+}
diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 8f0d4ad2bf..95e23a5616 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -57,6 +57,7 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.framework.interfaces.Broadcaster;
 import org.myrobotlab.framework.interfaces.ConfigurableService;
@@ -474,22 +475,15 @@ static public String getResourceDir(Class<?> clazz, String additionalPath) {
    *         then it needs an instance of Runtime which is not available.
    * 
    */
-  @Deprecated /* this should not be static - remove it */
   static public String getResourceDir(String serviceType, String additionalPath) {
 
     // setting resource directory
-    String resourceDir = null;
+    String resource = ConfigUtils.getResourceRoot() + fs + serviceType;
 
-    // stupid solution to get past static problem
-    if (!"Runtime".equals(serviceType)) {
-      resourceDir = Runtime.getInstance().getConfig().resource + fs + serviceType;
-    } else {
-      resourceDir = "resource";
-    }
     if (additionalPath != null) {
-      resourceDir = FileIO.gluePaths(resourceDir, additionalPath);
+      resource = FileIO.gluePaths(resource, additionalPath);
     }
-    return resourceDir;
+    return resource;
   }
 
   /**
@@ -516,7 +510,7 @@ public String getResourcePath(String additionalPath) {
    */
 
   static public String getResourceRoot() {
-    return Runtime.getInstance().getConfig().resource;
+    return ConfigUtils.getResourceRoot();//Runtime.getInstance().getConfig().resource;
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/io/FileIO.java b/src/main/java/org/myrobotlab/io/FileIO.java
index 2cdad66af2..8b3fe189c4 100644
--- a/src/main/java/org/myrobotlab/io/FileIO.java
+++ b/src/main/java/org/myrobotlab/io/FileIO.java
@@ -58,8 +58,8 @@
 import java.util.zip.ZipException;
 
 import org.apache.commons.io.Charsets;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.Platform;
-import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
@@ -854,8 +854,6 @@ public static void main(String[] args) throws ZipException, IOException {
       f = new File(uri);
       log.info("{} exists {}", uri, f.exists());
 
-      log.info("isJar : {}", isJar());
-
     } catch (Exception e) {
       Logging.logError(e);
     }
@@ -870,33 +868,22 @@ public static void main(String[] args) throws ZipException, IOException {
    *          Python/examples/someFile.py
    * @return byte array
    */
-  @Deprecated /* user Service.getResource(src) */
   static public final byte[] resourceToByteArray(String src) {
 
-    // this path assumes in a jar ?
-    // String filename = "/resource/" + src;
-    log.info("looking for Resource {}", src);
+    log.info("looking for resource {}", src);
     InputStream isr = null;
-    if (isJar()) {
-      // this path assumes in a jar ? ensure it's forward slashes
-      String filename = "/resource/" + src.replace("\\", "/");
-      isr = FileIO.class.getResourceAsStream(filename);
-    } else {
-      String localFilename = Service.getResourceRoot() + File.separator + src;
-      try {
-        isr = new FileInputStream(localFilename);
-      } catch (Exception e) {
-        Logging.logError(e);
-        log.error("File not found. {}", localFilename, e);
-        return null;
-      }
+    String resource = ConfigUtils.getResourceRoot();
+    String localFilename = resource + File.separator + src;
+    try {
+      isr = new FileInputStream(localFilename);
+    } catch (Exception e) {
+      Logging.logError(e);
+      log.error("file not found. {}", localFilename, e);
+      return null;
     }
+
     byte[] data = null;
     try {
-      if (isr == null) {
-        log.error("can not find resource [{}]", src);
-        return null;
-      }
       data = toByteArray(isr);
     } finally {
       try {
@@ -918,7 +905,6 @@ static public final byte[] resourceToByteArray(String src) {
    *          Python/examples/someFile.py
    * @return string
    */
-  @Deprecated /* use Service.getResourceAsString(src) */
   static public final String resourceToString(String src) {
     byte[] bytes = resourceToByteArray(src);
     if (bytes == null) {
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 146b72e68d..0248333656 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -890,13 +890,18 @@ public Object getPredicate(String key) {
     return null;
   }
 
+  /**
+   * getResponse from ProgramAB
+   * @param text
+   * @return
+   */
   public Response getResponse(String text) {
     ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
       Response response = chatBot.getResponse(text);
       return response;
     } else {
-      log.info("chatbot not ready");
+      log.warn("chatbot not ready");
     }
     return null;
   }
@@ -2044,6 +2049,13 @@ public boolean setSpeechType(String speechType) {
     // updatePeerType("mouth" /* getPeerName("mouth") */, speechType);
     // return speechType;
   }
+  
+  public void setTopic(String topic) {
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    if (chatBot != null) {
+      chatBot.setTopic(topic);
+    }
+  }
 
   public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) {
     sendToPeer("torso", "setSpeed", topStom, midStom, lowStom);
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 2b4b40e59f..a989e7046a 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -229,7 +229,7 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
   /**
    * default parent path of configPath static !
    */
-  final static protected String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
+  public final static String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
 
   /**
    * number of services created by this runtime
@@ -926,6 +926,10 @@ public static Runtime getInstance() {
               Runtime.startConfig(options.config);
             } else if (startYml != null && startYml.config != null && startYml.enable) {
               Runtime.startConfig(startYml.config);
+            } else {
+              RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {
+              });
+              runtime.apply(rtConfig);
             }
           } catch (Exception e) {
             log.info("runtime will not be loading config");
@@ -5408,4 +5412,12 @@ public static void removeConfig(String configName) {
     }
   }
 
+  /**
+   * Method used to determine is runtime is running without starting it
+   * @return true if available
+   */
+  static public boolean isAvailable() {
+    return runtime != null && runtime.isRunning();
+  }
+
 }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 1ec4694f0e..13eefd7b56 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -266,8 +266,6 @@ public Plan getDefault(Plan plan, String name) {
       }
     }
 
-    chatBot.currentUserName = "human";
-
     chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
 
     Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3"));
diff --git a/src/test/java/org/myrobotlab/io/FileIOTest.java b/src/test/java/org/myrobotlab/io/FileIOTest.java
index a8700219a8..c554835175 100644
--- a/src/test/java/org/myrobotlab/io/FileIOTest.java
+++ b/src/test/java/org/myrobotlab/io/FileIOTest.java
@@ -134,11 +134,6 @@ public void testGluePaths() {
     assertEquals("/abc/def/", ret);
   }
 
-  @Test
-  public void testIsJar() {
-    assertFalse(FileIO.isJar());
-  }
-
   @Test
   public void testGetFileListString() throws IOException {
     String dir = FileIO.gluePaths(tempDir, "testGetFileListString");

From 565f9ddf6288e59c1b742ba5f58cb8030f408eea Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 30 Jan 2024 05:18:11 -0800
Subject: [PATCH 015/131] set chatBot reference on startService added setup to
 fsm

---
 .../java/org/myrobotlab/service/InMoov2.java  | 95 ++++++++++---------
 .../service/config/InMoov2Config.java         |  8 +-
 2 files changed, 55 insertions(+), 48 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 0248333656..3db43b416e 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -739,7 +739,7 @@ public String execGesture(String gesture) {
     }
     return python.evalAndWait(gesture);
   }
-  
+
   /**
    * Reload the InMoov2.py script
    */
@@ -747,7 +747,6 @@ public void execScript() {
     execScript("InMoov2.py");
   }
 
-
   /**
    * FIXME - I think there was lots of confusion of executing resources or just
    * a file on the file system ... "execScript" I would expect to be just a file
@@ -762,8 +761,8 @@ public void execScript() {
    * @return success or failure
    */
   public void execScript(String someScriptName) {
-      String script = getResourceAsString(someScriptName);
-      invoke("publishPython", script);
+    String script = getResourceAsString(someScriptName);
+    invoke("publishPython", script);
   }
 
   public void finishedGesture() {
@@ -880,30 +879,23 @@ public OpenCV getOpenCV() {
     return opencv;
   }
 
-  public Object getPredicate(String key) {
-    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
-    if (chatBot != null) {
-      return chatBot.getPredicate(key);
-    } else {
-      error("no chatBot available");
-    }
-    return null;
+  public String getPredicate(String key) {
+    return getPredicate(chatBot.getConfig().currentUserName, key);
+  }
+
+  public String getPredicate(String user, String key) {
+    return chatBot.getPredicate(user, key);
   }
 
   /**
    * getResponse from ProgramAB
+   * 
    * @param text
    * @return
    */
   public Response getResponse(String text) {
-    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
-    if (chatBot != null) {
-      Response response = chatBot.getResponse(text);
-      return response;
-    } else {
-      log.warn("chatbot not ready");
-    }
-    return null;
+    Response response = chatBot.getResponse(text);
+    return response;
   }
 
   public InMoov2Arm getRightArm() {
@@ -965,6 +957,22 @@ public void halfSpeed() {
     sendToPeer("torso", "setSpeed", 20.0, 20.0, 20.0);
   }
 
+  /**
+   * If there have been any errors
+   * 
+   * @return
+   */
+  public boolean hasErrors() {
+    return errors.size() > 0;
+  }
+
+  /**
+   * clear all errors
+   */
+  public void clearErrors() {
+    errors.clear();
+  }
+
   public boolean isCameraOn() {
     if (opencv != null) {
       if (opencv.isCapturing()) {
@@ -1306,12 +1314,9 @@ public void onPeak(double volume) {
   public void onPirOn() {
     // FIXME flash on config.flashOnBoot
     invoke("publishFlash", "pir");
-    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
-    if (chatBot != null) {
-      String botState = chatBot.getPredicate("botState");
-      if ("sleeping".equals(botState)) {
-        invoke("publishEvent", "WAKE");
-      }
+    String botState = chatBot.getPredicate("botState");
+    if ("sleeping".equals(botState)) {
+      invoke("publishEvent", "WAKE");
     }
   }
 
@@ -1419,14 +1424,14 @@ public void onStartSpeaking(String utterance) {
    */
   public StateChange publishStateChange(StateChange stateChange) {
     log.info("publishStateChange {}", stateChange);
-    
+
     log.info("onStateChange {}", stateChange);
 
     lastState = state;
     state = stateChange.state;
 
     processMessage("onStateChange", stateChange);
-    
+
     return stateChange;
   }
 
@@ -1561,6 +1566,9 @@ public String publishFlash(String flashName) {
   }
 
   /**
+   * FIXME - get rid of all functionality here - should all be controlled by
+   * behaviors
+   * 
    * A heartbeat that continues to check status, and fire events to the FSM.
    * Checks battery, flashes leds and processes all the configured checks in
    * onHeartbeat at a regular interval
@@ -1635,7 +1643,7 @@ public Heartbeat publishHeartbeat() {
     processMessage("onHeartbeat", heartbeat);
     return heartbeat;
   }
-  
+
   /**
    * A more extensible interface point than publishEvent FIXME - create
    * interface for this
@@ -1807,7 +1815,6 @@ public void releasePeer(String peerKey) {
       invoke("publishEvent", "STOPPED " + peerKey);
     }
   }
-  
 
   @Override
   public void releaseService() {
@@ -1984,15 +1991,10 @@ public boolean setPirPlaySounds(boolean b) {
   }
 
   public Object setPredicate(String key, Object data) {
-    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
-    if (chatBot != null) {
-      if (data == null) {
-        chatBot.setPredicate(key, null); // "unknown" "null" other sillyness ?
-      } else {
-        chatBot.setPredicate(key, data.toString());
-      }
+    if (data == null) {
+      chatBot.setPredicate(key, null); // "unknown" "null" other sillyness ?
     } else {
-      error("no chatBot available");
+      chatBot.setPredicate(key, data.toString());
     }
     return data;
   }
@@ -2049,12 +2051,9 @@ public boolean setSpeechType(String speechType) {
     // updatePeerType("mouth" /* getPeerName("mouth") */, speechType);
     // return speechType;
   }
-  
+
   public void setTopic(String topic) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
-    if (chatBot != null) {
-      chatBot.setTopic(topic);
-    }
+    chatBot.setTopic(topic);
   }
 
   public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) {
@@ -2159,7 +2158,6 @@ public void startBrain() {
   public ProgramAB startChatBot() {
 
     try {
-      chatBot = (ProgramAB) startPeer("chatBot");
 
       if (locale != null) {
         chatBot.setCurrentBotName(locale.getTag());
@@ -2309,6 +2307,15 @@ public void startService() {
     // a FSM ProgramAB and some form of Python/Jython
     fsm = (FiniteStateMachine) startPeer("fsm");
 
+    // Chatbot is a required part of InMoov2
+    chatBot = (ProgramAB) startPeer("chatBot");
+    try {
+      chatBot.startSession();
+      chatBot.setPredicate("robot", getName());
+    } catch (IOException e) {
+      error(e);
+    }
+
     // A python process is required - should be defined as a peer
     // of Type Python or Py4j
 
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 13eefd7b56..e567016cdc 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -362,16 +362,16 @@ public Plan getDefault(Plan plan, String name) {
     // exists ?
     fsm.current = "boot";
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
-    fsm.transitions.add(new Transition("wake", "idle", "idle"));
-    fsm.transitions.add(new Transition("first_init", "idle", "idle"));
+    // fsm.transitions.add(new Transition("wake", "idle", "idle")); wake, setup, nor sleep should be affected by idle
+    fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
     fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("random", "idle", "idle"));
     fsm.transitions.add(new Transition("idle", "sleep", "sleep"));
     fsm.transitions.add(new Transition("sleep", "wake", "wake"));
     fsm.transitions.add(new Transition("sleep", "power_down", "power_down"));
     fsm.transitions.add(new Transition("idle", "power_down", "power_down"));
-    fsm.transitions.add(new Transition("wake", "first_init", "first_init"));
-    fsm.transitions.add(new Transition("idle", "first_init", "first_init"));
+    fsm.transitions.add(new Transition("wake", "setup", "setup"));
+    fsm.transitions.add(new Transition("idle", "setup", "setup"));
     // power_down to shutdown
     // fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished",
     // "awake"));

From d2510bc16a2b4e995e4258a39c4d4a7a7217695b Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 1 Feb 2024 08:28:33 -0800
Subject: [PATCH 016/131] working processor action in servomixer

---
 .../org/myrobotlab/kinematics/Action.java     |   7 +
 .../java/org/myrobotlab/service/Py4j.java     |   7 +-
 .../java/org/myrobotlab/service/Python.java   |   3 +-
 .../org/myrobotlab/service/ServoMixer.java    | 681 +++++++++---------
 .../service/config/ServoMixerConfig.java      |   5 +
 .../service/interfaces/Processor.java         |  18 +
 .../WebGui/app/service/js/ServoMixerGui.js    |   8 +
 .../app/service/views/ServoMixerGui.html      |   8 +-
 8 files changed, 392 insertions(+), 345 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/service/interfaces/Processor.java

diff --git a/src/main/java/org/myrobotlab/kinematics/Action.java b/src/main/java/org/myrobotlab/kinematics/Action.java
index afb159ffcb..6eb4108fda 100644
--- a/src/main/java/org/myrobotlab/kinematics/Action.java
+++ b/src/main/java/org/myrobotlab/kinematics/Action.java
@@ -57,4 +57,11 @@ public static Action createGestureToAction(String gestureName) {
     action.value = gestureName;
     return action;
   }
+
+  public static Action createProcessingAction(String methodName) {
+    Action action = new Action();
+    action.type = "process";
+    action.value = methodName;
+    return action;
+  }
 }
diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java
index 16b335f455..d5530fc50d 100644
--- a/src/main/java/org/myrobotlab/service/Py4j.java
+++ b/src/main/java/org/myrobotlab/service/Py4j.java
@@ -25,6 +25,7 @@
 import org.myrobotlab.service.data.Script;
 import org.myrobotlab.service.interfaces.Executor;
 import org.myrobotlab.service.interfaces.Gateway;
+import org.myrobotlab.service.interfaces.Processor;
 import org.slf4j.Logger;
 
 import py4j.GatewayServer;
@@ -54,7 +55,7 @@
  * 
  * @author GroG
  */
-public class Py4j extends Service<Py4jConfig> implements GatewayServerListener, Gateway {
+public class Py4j extends Service<Py4jConfig> implements GatewayServerListener, Gateway, Processor {
 
   /**
    * POJO class to tie all the data elements of a external python process
@@ -235,17 +236,19 @@ public void connectionStopped(Py4JServerConnection gatewayConnection) {
    * 
    * @param code The Python code to execute in the interpreter.
    */
-  public void exec(String code) {
+  public boolean exec(String code) {
     log.info(String.format("exec %s", code));
     try {
       if (handler != null) {
         handler.exec(code);
+        return true;
       } else {
         error("handler is null");
       }
     } catch (Exception e) {
       error(e);
     }
+    return false;
   }
 
   private String getClientKey(Py4JServerConnection gatewayConnection) {
diff --git a/src/main/java/org/myrobotlab/service/Python.java b/src/main/java/org/myrobotlab/service/Python.java
index 90502146f2..77fd9fd529 100644
--- a/src/main/java/org/myrobotlab/service/Python.java
+++ b/src/main/java/org/myrobotlab/service/Python.java
@@ -25,6 +25,7 @@
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.config.PythonConfig;
 import org.myrobotlab.service.data.Script;
+import org.myrobotlab.service.interfaces.Processor;
 import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
 import org.myrobotlab.service.meta.abstracts.MetaData;
 import org.python.core.Py;
@@ -49,7 +50,7 @@
  * @author GroG
  * 
  */
-public class Python extends Service<PythonConfig> implements ServiceLifeCycleListener, MessageListener {
+public class Python extends Service<PythonConfig> implements ServiceLifeCycleListener, MessageListener, Processor {
   
   /**
    * this thread handles all callbacks to Python process all input and sets msg
diff --git a/src/main/java/org/myrobotlab/service/ServoMixer.java b/src/main/java/org/myrobotlab/service/ServoMixer.java
index 644557185b..5e20888104 100755
--- a/src/main/java/org/myrobotlab/service/ServoMixer.java
+++ b/src/main/java/org/myrobotlab/service/ServoMixer.java
@@ -15,7 +15,6 @@
 import java.util.concurrent.Executors;
 
 import org.myrobotlab.codec.CodecUtils;
-import org.myrobotlab.framework.Message;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.Attachable;
@@ -28,6 +27,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.config.ServoMixerConfig;
+import org.myrobotlab.service.interfaces.Processor;
 import org.myrobotlab.service.interfaces.SelectListener;
 import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
 import org.myrobotlab.service.interfaces.ServoControl;
@@ -44,31 +44,6 @@
  */
 public class ServoMixer extends Service<ServoMixerConfig> implements ServiceLifeCycleListener, SelectListener {
 
-  public class PlayingGesture {
-    public String name;
-    public Gesture gesture;
-    public int startIndex = 0;
-
-    public PlayingGesture(String name, Gesture gesture) {
-      this(name, gesture, 0);
-    }
-
-    public PlayingGesture(String name, Gesture gesture, int index) {
-      this.name = name;
-      this.gesture = gesture;
-      this.startIndex = index;
-    }
-
-    public String toString() {
-      int actionCnt = 0;
-      if (gesture != null && gesture.actions != null) {
-        actionCnt = gesture.actions.size();
-      }
-      return String.format("name:%s actionCnt:%d index:%d", name, actionCnt, startIndex);
-    }
-
-  }
-
   /**
    * The Player plays a requested gesture, which is a sequence of Poses. Poses
    * can be positions, delays, or speech. It publishes when it starts a gesture
@@ -78,10 +53,10 @@ public String toString() {
    *
    */
   public class Player implements Runnable {
-    protected String playingGesture = null;
-    protected boolean running = false;
     transient private ExecutorService executor;
+    protected String playingGesture = null;
     transient Deque<PlayingGesture> playStack = new ArrayDeque<>();
+    protected boolean running = false;
 
     // FIXME - add optional start index to start playing at a midpoint
     private void play() {
@@ -188,8 +163,17 @@ public void processAction(PlayingGesture current, int i) {
         case "speak":
           speak((Map) action.value);
           break;
-        case "msg":
-          invoke("publishProcessMessage", (Message) action.value);
+        case "process":
+
+          if (config.processor != null) {
+            Processor processor = (Processor) Runtime.getService(config.processor);
+            String code = (String) action.value;
+            if (!processor.exec(code)) {
+              error("could not execute %s", code);
+            }
+          } else {
+            error("no processor");
+          }
           break;
         default: {
           error("do not know how to handle gesture part of type %s", action.type);
@@ -232,16 +216,102 @@ public void stop() {
     }
   }
 
+  public class PlayingGesture {
+    public Gesture gesture;
+    public String name;
+    public int startIndex = 0;
+
+    public PlayingGesture(String name, Gesture gesture) {
+      this(name, gesture, 0);
+    }
+
+    public PlayingGesture(String name, Gesture gesture, int index) {
+      this.name = name;
+      this.gesture = gesture;
+      this.startIndex = index;
+    }
+
+    public String toString() {
+      int actionCnt = 0;
+      if (gesture != null && gesture.actions != null) {
+        actionCnt = gesture.actions.size();
+      }
+      return String.format("name:%s actionCnt:%d index:%d", name, actionCnt, startIndex);
+    }
+
+  }
+
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
   private static final long serialVersionUID = 1L;
 
+  public static void main(String[] args) throws Exception {
+
+    try {
+      Runtime.main(new String[] { "--id", "admin" });
+      LoggingFactory.init("INFO");
+
+      Runtime.start("i01.head.rothead", "Servo");
+      Runtime.start("i01.head.neck", "Servo");
+      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
+      webgui.autoStartBrowser(false);
+      webgui.startService();
+      Python python = (Python) Runtime.start("python", "Python");
+      ServoMixer mixer = (ServoMixer) Runtime.start("mixer", "ServoMixer");
+      // mixer.playGesture("");
+
+      boolean done = true;
+      if (done) {
+        return;
+      }
+
+      mixer.addNewGestureFile("test");
+      Gesture gesture = mixer.getGesture("test");
+      String gestureName = "aaa";
+      String servoName = "i01.head.rothead";
+      Double position = 90.0;
+      Double speed = null;
+
+      Map<String, Map<String, Object>> moves = new TreeMap<>();
+
+      Map<String, Object> poseMove1 = new TreeMap<>();
+      poseMove1.put("position", 90.0);
+
+      Map<String, Object> poseMove2 = new TreeMap<>();
+      poseMove2.put("position", 90);
+      poseMove2.put("speed", 35.0);
+
+      moves.put("i01.head.rothead", poseMove1);
+      moves.put("i01.head.neck", poseMove2);
+
+      mixer.openGesture(gestureName);
+      mixer.addMoveToAction(moves); // autofill delay from keyframe ?
+      mixer.saveGesture();
+
+      mixer.addNewGestureFile("test2");
+
+      // mixer.save(gestureName);
+      mixer.addGestureToAction("test");
+
+      mixer.saveGesture();
+
+      // mixer.setPose("test", )
+    } catch (Exception e) {
+      log.error("main threw", e);
+    }
+  }
+
+  // TODO selected servos
+
   /**
    * Set of servo names kept in sync with current registry
    */
   protected Set<String> allServos = new TreeSet<>();
 
-  // TODO selected servos
+  /**
+   * gesture name of the currentGesture
+   */
+  protected String currentEditGestureName = null;
 
   /**
    * current gesture being edited
@@ -253,13 +323,67 @@ public void stop() {
    */
   final protected transient Player player = new Player();
 
+  public ServoMixer(String n, String id) {
+    super(n, id);
+  }
+
+  public void addAction(Action action, Integer index) {
+    if (currentGesture == null) {
+      error("current gesture not set");
+      return;
+    }
+    if (index != null) {
+      if (currentGesture.actions.size() == 0) {
+        currentGesture.actions.add(action);
+      } else {
+        currentGesture.actions.add(index, action);
+      }
+    } else {
+      currentGesture.actions.add(action);
+    }
+  }
+
+  public void addGestureToAction(String gestureName) {
+    addGestureToAction(gestureName, null);
+  };
+
+  public void addGestureToAction(String gestureName, Integer index) {
+    addAction(Action.createGestureToAction(gestureName), index);
+  }
+
+  public void addMoveToAction(List<String> servos) {
+    addMoveToAction(servos, null);
+  }
+
   /**
-   * gesture name of the currentGesture
+   * list of servos to add to an action - they're current speed and input
+   * positions will be added at index
+   * 
+   * @param servos
+   * @param index
    */
-  protected String currentEditGestureName = null;
+  public void addMoveToAction(List<String> servos, Integer index) {
+    Map<String, Map<String, Object>> moves = new TreeMap<>();
+    for (String servoName : servos) {
+      ServoControl sc = (ServoControl) Runtime.getService(servoName);
+      Map<String, Object> posAndSpeed = new TreeMap<>();
+      if (sc == null) {
+        error("%s not a valid service name", servoName);
+        continue;
+      }
+      posAndSpeed.put("position", sc.getCurrentInputPos());
+      posAndSpeed.put("speed", sc.getSpeed());
+      moves.put(servoName, posAndSpeed);
+    }
+    addAction(Action.createMoveToAction(moves), index);
+  }
 
-  public ServoMixer(String n, String id) {
-    super(n, id);
+  public void addMoveToAction(Map<String, Map<String, Object>> moves) {
+    addMoveToAction(moves, null);
+  }
+
+  public void addMoveToAction(Map<String, Map<String, Object>> moves, Integer index) {
+    addAction(Action.createMoveToAction(moves), index);
   }
 
   /**
@@ -294,10 +418,28 @@ public String addNewGestureFile(String name) {
     return filename;
   }
 
-  public void saveGesture(String name) {
-    // FIXME - warn if overwrite
-    // FIXME - don't warn if opened
-    saveGesture(name, currentGesture);
+  public void addProcessingAction(String methodName) {
+    addProcessingAction(methodName, null);
+  }
+
+  public void addProcessingAction(String methodName, Integer index) {
+    addAction(Action.createProcessingAction(methodName), index);
+  }
+
+  public void addSleepAction(double sleep) {
+    addSleepAction(sleep, null);
+  }
+
+  public void addSleepAction(double sleep, Integer index) {
+    addAction(Action.createSleepAction(sleep), index);
+  }
+
+  public void addSpeakAction(Map<String, Object> speechCommand) {
+    addSpeakAction(speechCommand, null);
+  }
+
+  public void addSpeakAction(Map<String, Object> speechCommand, Integer index) {
+    addAction(Action.createSpeakAction(speechCommand), index);
   }
 
   /**
@@ -308,24 +450,14 @@ public void attach(Attachable attachable) {
     if (attachable instanceof Servo) {
       attachServo((Servo) attachable);
     }
-  };
+  }
 
   /**
    * attach(String) should always have the implementation
    */
   @Override
   public void attach(String name) {
-      allServos.add(name);
-      // refresh subscribers
-      invoke("getServos");
-  }
-  
-  /**
-   * detach(String) should always have the implementation
-   */
-  @Override
-  public void detach(String name) {
-    allServos.remove(name);
+    allServos.add(name);
     // refresh subscribers
     invoke("getServos");
   }
@@ -341,6 +473,20 @@ public void attachServo(Servo servo) {
     attach(servo.getName());
   }
 
+  /**
+   * detach(String) should always have the implementation
+   */
+  @Override
+  public void detach(String name) {
+    allServos.remove(name);
+    // refresh subscribers
+    invoke("getServos");
+  }
+
+  public Gesture getGesture() {
+    return currentGesture;
+  }
+
   public Gesture getGesture(String name) {
 
     Gesture gesture = null;
@@ -404,9 +550,10 @@ public List<String> getGestureFiles() {
   public String getPosesDirectory() {
     return config.posesDir;
   }
-  
+
   /**
    * get a refreshed ordered list of servos
+   * 
    * @return
    */
   public Set<String> getServos() {
@@ -431,34 +578,61 @@ public List<ServoControl> listAllServos() {
     return servos;
   }
 
-  public void step(int index) {
-    step(currentEditGestureName, index);
-  }
-
-  public void step(String gestureName, int index) {
-
-    if (gestureName == null) {
-      error("gesture name cannot be null");
+  public void moveActionDown(int index) {
+    if (currentGesture == null) {
+      error("cannot move: gesture not set");
       return;
     }
 
-    if (!gestureName.equals(currentEditGestureName)) {
-      // load gesture
-      getGesture(gestureName);
+    List<Action> list = currentGesture.actions;
+
+    if (index >= 0 && index < list.size() - 1) {
+      Action action = list.remove(index);
+      list.add(index + 1, action);
+    } else {
+      error("index out of range or at the end of the list.");
     }
+    invoke("getGesture");
+  }
 
+  public void moveActionUp(int index) {
     if (currentGesture == null) {
-      error("gesture cannot be nulle");
+      error("cannot move gesture not set");
       return;
     }
 
-    player.processAction(new PlayingGesture(gestureName, currentGesture), index);
-    // step to next action
-    index++;
-    if (index < currentGesture.actions.size()) {
-      Action action = currentGesture.actions.get(index);
-      invoke("publishPlayingAction", action);
-      invoke("publishPlayingActionIndex", index);
+    List<Action> list = currentGesture.actions;
+
+    if (index > 0 && index < list.size()) {
+      Action action = list.remove(index);
+      list.add(index - 1, action);
+    } else {
+      error("index out of range or at the beginning of the list.");
+    }
+    invoke("getGesture");
+  }
+
+  private void moveTo(String servoName, Map<String, Object> move) {
+    ServoControl servo = (ServoControl) Runtime.getService(servoName);
+    if (servo == null) {
+      warn("servo (%s) cannot move to pose because it does not exist", servoName);
+      return;
+    }
+
+    Object speed = move.get("speed");
+    if (speed != null) {
+      if (speed instanceof Integer) {
+        servo.setSpeed((Integer) speed);
+      } else if (speed instanceof Double) {
+        servo.setSpeed((Double) speed);
+      }
+    }
+
+    Object position = move.get("position");
+    if (position instanceof Integer) {
+      servo.moveTo((Integer) position);
+    } else if (position instanceof Double) {
+      servo.moveTo((Double) position);
     }
   }
 
@@ -534,6 +708,24 @@ public void onStarted(String name) {
   public void onStopped(String name) {
   }
 
+  public Gesture openGesture(String name) {
+    if (currentGesture != null) {
+      warn("replacing current gesture");
+      // prompt user return null etc.
+    }
+
+    Gesture gesture = getGesture(name);
+    if (gesture == null) {
+      // gesture not found make new
+      gesture = new Gesture();
+    }
+
+    currentEditGestureName = name;
+    currentGesture = gesture;
+
+    return currentGesture;
+  }
+
   public void playGesture(String name) {
     // Gesture gesture = (Gesture) broadcast("getGesture", name);
     invoke("getGesture", name);
@@ -585,17 +777,6 @@ public String publishPlayingPose(String name) {
     return name;
   }
 
-  /**
-   * Processing publishing point, where everything InMoov2 wants to be processed
-   * is turned into a message and published.
-   * 
-   * @param msg
-   * @return
-   */
-  public Message publishProcessMessage(Message msg) {
-    return msg;
-  }
-
   public String publishStopPose(String name) {
     return name;
   }
@@ -611,6 +792,23 @@ public String publishText(String text) {
     return text;
   }
 
+  public void removeActionFromGesture(int index) {
+    try {
+
+      Gesture gesture = getGesture();
+      if (gesture == null) {
+        error("gesture not set");
+        return;
+      }
+      if (gesture.actions.size() != 0 && index < gesture.actions.size()) {
+        gesture.actions.remove(index);
+      }
+
+    } catch (Exception e) {
+      error(e);
+    }
+  }
+
   public void removeGesture(String name) {
 
     try {
@@ -632,57 +830,6 @@ public void removeGesture(String name) {
     }
   }
 
-  public void moveActionUp(int index) {
-    if (currentGesture == null) {
-      error("cannot move gesture not set");
-      return;
-    }
-
-    List<Action> list = currentGesture.actions;
-
-    if (index > 0 && index < list.size()) {
-      Action action = list.remove(index);
-      list.add(index - 1, action);
-    } else {
-      error("index out of range or at the beginning of the list.");
-    }
-    invoke("getGesture");
-  }
-
-  public void moveActionDown(int index) {
-    if (currentGesture == null) {
-      error("cannot move: gesture not set");
-      return;
-    }
-
-    List<Action> list = currentGesture.actions;
-
-    if (index >= 0 && index < list.size() - 1) {
-      Action action = list.remove(index);
-      list.add(index + 1, action);
-    } else {
-      error("index out of range or at the end of the list.");
-    }
-    invoke("getGesture");
-  }
-
-  public void removeActionFromGesture(int index) {
-    try {
-
-      Gesture gesture = getGesture();
-      if (gesture == null) {
-        error("gesture not set");
-        return;
-      }
-      if (gesture.actions.size() != 0 && index < gesture.actions.size()) {
-        gesture.actions.remove(index);
-      }
-
-    } catch (Exception e) {
-      error(e);
-    }
-  }
-
   public void rest() {
     for (String servo : allServos) {
       Servo s = (Servo) Runtime.getService(servo);
@@ -690,6 +837,17 @@ public void rest() {
     }
   }
 
+  public void saveGesture() {
+    // FIXME check if exists - ask overwrite
+    saveGesture(currentEditGestureName, currentGesture);
+  }
+
+  public void saveGesture(String name) {
+    // FIXME - warn if overwrite
+    // FIXME - don't warn if opened
+    saveGesture(name, currentGesture);
+  }
+
   /**
    * Takes name of a file and a json encoded string of a gesture, saves it to
    * file and sets the "current" gesture to the data
@@ -767,6 +925,32 @@ public void setPosesDirectory(String posesDirectory) {
     broadcastState();
   }
 
+  private void speak(Map<String, Object> speechPart) {
+    if (config.mouth == null) {
+      warn("mouth configuration not set");
+      return;
+    }
+    SpeechSynthesis mouth = (SpeechSynthesis) Runtime.getService(config.mouth);
+    if (mouth == null) {
+      error("%s speech synthesis service missing", config.mouth);
+      return;
+    }
+    try {
+      // makes it harder to block
+      // FIXME if blocking send(mouthName, "speak")
+      // TODO - show multiple SpeechSynthesis select like Servos
+      Boolean blocking = (Boolean) speechPart.get("blocking");
+      // if (blocking != null && blocking) {
+      mouth.speakBlocking((String) speechPart.get("text")); // default blocking
+      // } else {
+      // mouth.speak((String) speechPart.get("text"));
+      // }
+    } catch (Exception e) {
+      error(e);
+    }
+
+  }
+
   @Override
   public void startService() {
     try {
@@ -790,225 +974,48 @@ public void startService() {
     }
   }
 
-  /**
-   * stop the current running gesture
-   */
-  public void stop() {
-    player.stop();
-  }
-
-  @Override
-  public void stopService() {
-    super.stopService();
-    player.stop();
-  }
-
-  public static void main(String[] args) throws Exception {
-
-    try {
-      Runtime.main(new String[] { "--id", "admin" });
-      LoggingFactory.init("INFO");
-
-      Runtime.start("i01.head.rothead", "Servo");
-      Runtime.start("i01.head.neck", "Servo");
-      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
-      webgui.autoStartBrowser(false);
-      webgui.startService();
-      Python python = (Python) Runtime.start("python", "Python");
-      ServoMixer mixer = (ServoMixer) Runtime.start("mixer", "ServoMixer");
-      // mixer.playGesture("");
-
-      boolean done = true;
-      if (done) {
-        return;
-      }
-
-      mixer.addNewGestureFile("test");
-      Gesture gesture = mixer.getGesture("test");
-      String gestureName = "aaa";
-      String servoName = "i01.head.rothead";
-      Double position = 90.0;
-      Double speed = null;
-
-      Map<String, Map<String, Object>> moves = new TreeMap<>();
-
-      Map<String, Object> poseMove1 = new TreeMap<>();
-      poseMove1.put("position", 90.0);
-
-      Map<String, Object> poseMove2 = new TreeMap<>();
-      poseMove2.put("position", 90);
-      poseMove2.put("speed", 35.0);
-
-      moves.put("i01.head.rothead", poseMove1);
-      moves.put("i01.head.neck", poseMove2);
-
-      mixer.openGesture(gestureName);
-      mixer.addMoveToAction(moves); // autofill delay from keyframe ?
-      mixer.saveGesture();
-
-      mixer.addNewGestureFile("test2");
-
-      // mixer.save(gestureName);
-      mixer.addGestureToAction("test");
-
-      mixer.saveGesture();
-
-      // mixer.setPose("test", )
-    } catch (Exception e) {
-      log.error("main threw", e);
-    }
-  }
-
-  public void addGestureToAction(String gestureName) {
-    addGestureToAction(gestureName, null);
-  }
-
-  public void addGestureToAction(String gestureName, Integer index) {
-    addAction(Action.createGestureToAction(gestureName), index);
-  }
-
-  public void saveGesture() {
-    // FIXME check if exists - ask overwrite
-    saveGesture(currentEditGestureName, currentGesture);
+  public void step(int index) {
+    step(currentEditGestureName, index);
   }
 
-  public Gesture openGesture(String name) {
-    if (currentGesture != null) {
-      warn("replacing current gesture");
-      // prompt user return null etc.
-    }
+  public void step(String gestureName, int index) {
 
-    Gesture gesture = getGesture(name);
-    if (gesture == null) {
-      // gesture not found make new
-      gesture = new Gesture();
+    if (gestureName == null) {
+      error("gesture name cannot be null");
+      return;
     }
 
-    currentEditGestureName = name;
-    currentGesture = gesture;
-
-    return currentGesture;
-  }
-
-  public void addSpeakAction(Map<String, Object> speechCommand) {
-    addSpeakAction(speechCommand, null);
-  }
-
-  public void addSpeakAction(Map<String, Object> speechCommand, Integer index) {
-    addAction(Action.createSpeakAction(speechCommand), index);
-  }
-
-  public void addMoveToAction(List<String> servos) {
-    addMoveToAction(servos, null);
-  }
-
-  /**
-   * list of servos to add to an action - they're current speed and input
-   * positions will be added at index
-   * 
-   * @param servos
-   * @param index
-   */
-  public void addMoveToAction(List<String> servos, Integer index) {
-    Map<String, Map<String, Object>> moves = new TreeMap<>();
-    for (String servoName : servos) {
-      ServoControl sc = (ServoControl) Runtime.getService(servoName);
-      Map<String, Object> posAndSpeed = new TreeMap<>();
-      if (sc == null) {
-        error("%s not a valid service name", servoName);
-        continue;
-      }
-      posAndSpeed.put("position", sc.getCurrentInputPos());
-      posAndSpeed.put("speed", sc.getSpeed());
-      moves.put(servoName, posAndSpeed);
+    if (!gestureName.equals(currentEditGestureName)) {
+      // load gesture
+      getGesture(gestureName);
     }
-    addAction(Action.createMoveToAction(moves), index);
-  }
-
-  public void addMoveToAction(Map<String, Map<String, Object>> moves) {
-    addMoveToAction(moves, null);
-  }
-
-  public void addMoveToAction(Map<String, Map<String, Object>> moves, Integer index) {
-    addAction(Action.createMoveToAction(moves), index);
-  }
 
-  public void addAction(Action action, Integer index) {
     if (currentGesture == null) {
-      error("current gesture not set");
-      return;
-    }
-    if (index != null) {
-      if (currentGesture.actions.size() == 0) {
-        currentGesture.actions.add(action);
-      } else {
-        currentGesture.actions.add(index, action);
-      }
-    } else {
-      currentGesture.actions.add(action);
-    }
-  }
-
-  public void addSleepAction(double sleep) {
-    addSleepAction(sleep, null);
-  }
-
-  public void addSleepAction(double sleep, Integer index) {
-    addAction(Action.createSleepAction(sleep), index);
-  }
-
-  public Gesture getGesture() {
-    return currentGesture;
-  }
-
-  private void moveTo(String servoName, Map<String, Object> move) {
-    ServoControl servo = (ServoControl) Runtime.getService(servoName);
-    if (servo == null) {
-      warn("servo (%s) cannot move to pose because it does not exist", servoName);
+      error("gesture cannot be nulle");
       return;
     }
 
-    Object speed = move.get("speed");
-    if (speed != null) {
-      if (speed instanceof Integer) {
-        servo.setSpeed((Integer) speed);
-      } else if (speed instanceof Double) {
-        servo.setSpeed((Double) speed);
-      }
-    }
-
-    Object position = move.get("position");
-    if (position instanceof Integer) {
-      servo.moveTo((Integer) position);
-    } else if (position instanceof Double) {
-      servo.moveTo((Double) position);
+    player.processAction(new PlayingGesture(gestureName, currentGesture), index);
+    // step to next action
+    index++;
+    if (index < currentGesture.actions.size()) {
+      Action action = currentGesture.actions.get(index);
+      invoke("publishPlayingAction", action);
+      invoke("publishPlayingActionIndex", index);
     }
   }
 
-  private void speak(Map<String, Object> speechPart) {
-    if (config.mouth == null) {
-      warn("mouth configuration not set");
-      return;
-    }
-    SpeechSynthesis mouth = (SpeechSynthesis) Runtime.getService(config.mouth);
-    if (mouth == null) {
-      error("%s speech synthesis service missing", config.mouth);
-      return;
-    }
-    try {
-      // makes it harder to block
-      // FIXME if blocking send(mouthName, "speak")
-      // TODO - show multiple SpeechSynthesis select like Servos
-      Boolean blocking = (Boolean) speechPart.get("blocking");
-      // if (blocking != null && blocking) {
-      mouth.speakBlocking((String) speechPart.get("text")); // default blocking
-      // } else {
-      // mouth.speak((String) speechPart.get("text"));
-      // }
-    } catch (Exception e) {
-      error(e);
-    }
+  /**
+   * stop the current running gesture
+   */
+  public void stop() {
+    player.stop();
+  }
 
+  @Override
+  public void stopService() {
+    super.stopService();
+    player.stop();
   }
 
 }
diff --git a/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java b/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java
index b62c1200a5..813b32ae8a 100644
--- a/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ServoMixerConfig.java
@@ -22,5 +22,10 @@ public class ServoMixerConfig extends ServiceConfig {
    * speech service name
    */
   public String mouth;
+
+  /**
+   * name of the default processor
+   */
+  public String processor = "python";
   
 }
diff --git a/src/main/java/org/myrobotlab/service/interfaces/Processor.java b/src/main/java/org/myrobotlab/service/interfaces/Processor.java
new file mode 100644
index 0000000000..8a32c2622a
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/interfaces/Processor.java
@@ -0,0 +1,18 @@
+package org.myrobotlab.service.interfaces;
+
+public interface Processor {
+
+  /**
+   * 
+   * FIXME - this should be refactored to return an Object
+   * but I don't want to break anything for now
+   * FIXME - if there is an error this should throw and return
+   * and object or null if successful
+   * 
+   * A processor can exec code
+   * @param code
+   * @return success or not
+   */
+  public boolean exec(String code);
+  
+}
diff --git a/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js b/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js
index 85dc0cbcf3..6c231adacb 100644
--- a/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/ServoMixerGui.js
@@ -241,6 +241,14 @@ angular.module("mrlapp.service.ServoMixerGui", []).controller("ServoMixerGuiCtrl
       msg.send("getGesture")
     }
 
+    $scope.addPython = function(methodName){
+      let index = parseInt($scope.state.gestureIndex) + 1
+        $scope.state.gestureIndex = index + ""
+      msg.send("addProcessingAction", methodName, index)
+      msg.send("getGesture")
+        
+    }
+
     $scope.playGesture = function (gesture) {
       if (gesture) {
         msg.send("playGesture", gesture)
diff --git a/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html b/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html
index 4e7b48072a..4e38b899b4 100644
--- a/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/ServoMixerGui.html
@@ -78,16 +78,14 @@ <h3>{{state.selectedGestureFile}} {{state.playingPose.name}} {{state.gestureInde
             </td>
             </tr>
 
-<!--
               <tr><td>
-        <button class="btn btn-default" ng-click="addSleep(sleep)" title="Add sleep in seconds to gesture" style="width: 100%;text-align: left;">
+        <button class="btn btn-default" ng-click="addPython(methodName)" title="Add a python method" style="width: 100%;text-align: left;">
           <span class="glyphicon glyphicon-plus"></span>
-          message
+          python
         </button></td><td>
-        <input class="form-control servo-mixer-pose" type="textarea" ng-model="text" placeholder='{"method":"playFile","data"["laugh.mp3"]}' />
+        <input class="form-control servo-mixer-pose" type="text" ng-model="methodName" placeholder='method_name' />
             </td>
             </tr>
--->
               
           </table>
           

From 3398f8a7744e001476da68385ea38e1309ff355a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 2 Feb 2024 06:18:31 -0800
Subject: [PATCH 017/131] fix runtime.resource for tests

---
 .../java/org/myrobotlab/service/Runtime.java   |  3 +--
 .../java/org/myrobotlab/test/AbstractTest.java | 18 ++++++++++++++++++
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 54f9462425..66cf15b322 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -2786,10 +2786,9 @@ public Runtime(String n, String id) {
           if (deps.size() == 0) {
             metaData.installed = true;
           } else {
-            warn("{} not installed", metaData.getSimpleName());
+            log.info("{} not installed", metaData.getSimpleName());
           }
         }
-        
       }
     }
 
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index c4fc33b83d..00cc6a202d 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -1,5 +1,7 @@
 package org.myrobotlab.test;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -13,10 +15,12 @@
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.rules.TestName;
+import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.RuntimeConfig;
 import org.slf4j.Logger;
 
 public class AbstractTest {
@@ -83,6 +87,20 @@ public static void main(String[] args) {
 
   @BeforeClass
   public static void setUpAbstractTest() throws Exception {
+    
+    // setup runtime resource = src/main/resources/resource
+    File runtimeYml = new File("data/conf/default/runtime.yml");
+    if (!runtimeYml.exists()) {
+      RuntimeConfig rc = new RuntimeConfig();
+      rc.resource = "src/main/resources/resource";
+      String yml = CodecUtils.toYaml(rc);
+      
+      FileOutputStream fos = null;
+      fos = new FileOutputStream(runtimeYml);
+      fos.write(yml.getBytes());
+      fos.close();
+      
+    }
 
     Platform.setVirtual(true);
 

From 557cd80986e66b871c19c47263ecedae18e7ae4d Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 2 Feb 2024 06:22:13 -0800
Subject: [PATCH 018/131] again

---
 src/test/java/org/myrobotlab/test/AbstractTest.java | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 00cc6a202d..3b3351c674 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -91,6 +91,7 @@ public static void setUpAbstractTest() throws Exception {
     // setup runtime resource = src/main/resources/resource
     File runtimeYml = new File("data/conf/default/runtime.yml");
     if (!runtimeYml.exists()) {
+      runtimeYml.getParentFile().mkdirs();
       RuntimeConfig rc = new RuntimeConfig();
       rc.resource = "src/main/resources/resource";
       String yml = CodecUtils.toYaml(rc);

From 0a3c81cf50dea1eb50576e4d5d2d585bd85005b4 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 2 Feb 2024 06:26:55 -0800
Subject: [PATCH 019/131] again

---
 src/test/java/org/myrobotlab/test/AbstractTest.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 3b3351c674..3ea2b7f589 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -90,7 +90,7 @@ public static void setUpAbstractTest() throws Exception {
     
     // setup runtime resource = src/main/resources/resource
     File runtimeYml = new File("data/conf/default/runtime.yml");
-    if (!runtimeYml.exists()) {
+//    if (!runtimeYml.exists()) {
       runtimeYml.getParentFile().mkdirs();
       RuntimeConfig rc = new RuntimeConfig();
       rc.resource = "src/main/resources/resource";
@@ -101,7 +101,7 @@ public static void setUpAbstractTest() throws Exception {
       fos.write(yml.getBytes());
       fos.close();
       
-    }
+//    }
 
     Platform.setVirtual(true);
 

From ce82eb5d6087b6b92391e3beea6eb05b147cd986 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 2 Feb 2024 06:30:31 -0800
Subject: [PATCH 020/131] corrected wrong path

---
 src/test/java/org/myrobotlab/test/AbstractTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 3ea2b7f589..6b7ec2b432 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -89,7 +89,7 @@ public static void main(String[] args) {
   public static void setUpAbstractTest() throws Exception {
     
     // setup runtime resource = src/main/resources/resource
-    File runtimeYml = new File("data/conf/default/runtime.yml");
+    File runtimeYml = new File("data/config/default/runtime.yml");
 //    if (!runtimeYml.exists()) {
       runtimeYml.getParentFile().mkdirs();
       RuntimeConfig rc = new RuntimeConfig();

From 0483fb827c3da507f8fa355fe32fa0887eca4de9 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 07:13:12 -0800
Subject: [PATCH 021/131] Subscription fixes in config - removal of hardcoded
 subscriptions

---
 .../org/myrobotlab/config/ConfigUtils.java    | 71 ++++++++++++++++
 .../java/org/myrobotlab/service/InMoov2.java  | 83 -------------------
 .../org/myrobotlab/service/InMoov2Arm.java    |  2 +-
 .../org/myrobotlab/service/InMoov2Hand.java   |  2 +-
 .../org/myrobotlab/service/InMoov2Head.java   |  2 +-
 .../org/myrobotlab/service/InMoov2Torso.java  |  2 +-
 .../service/config/InMoov2Config.java         | 12 +--
 7 files changed, 81 insertions(+), 93 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/config/ConfigUtils.java

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
new file mode 100644
index 0000000000..35c8a776a8
--- /dev/null
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -0,0 +1,71 @@
+package org.myrobotlab.config;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.io.FileIO;
+import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.RuntimeConfig;
+
+public class ConfigUtils {
+
+  /**
+   * This gets the current resource root without starting a Runtime instance if
+   * not already started. The resource root depends on config, if Runtime is
+   * running the logic and current config name is already available. If Runtime
+   * is not running, we need to go through a series of steps to deterime where
+   * the resource root is configured.
+   * 
+   * @return
+   */
+  public static String getResourceRoot() {
+
+    String resource = "resource";
+
+    // check if runtime is running
+    if (!Runtime.isAvailable()) {
+      // check for start.yml
+
+      File checkStartYml = new File("start.yml");
+      StartYml startYml = new StartYml();
+      if (checkStartYml.exists()) {
+        String yml;
+        try {
+          yml = FileIO.toString("start.yml");
+          startYml = CodecUtils.fromYaml(yml, StartYml.class);
+
+          // see if autostart is on with a config
+          if (startYml.enable) {
+            // use that config to find runtime.yml
+
+            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + startYml.config + File.separator + "runtime.yml");
+            if (runtimeYml.exists()) {
+              // parse that file look for resource: entry in file
+              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+              resource = config.resource;
+            }
+
+          } else {
+            // start.yml enable = false / so we'll use default config
+            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + "default" + File.separator + "runtime.yml");
+            if (runtimeYml.exists()) {
+              // parse that file look for resource: entry in file
+              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+              resource = config.resource;
+            }
+          }
+
+        } catch (IOException e) {
+          // problem getting or parsing
+          // going to assume default "resource"
+        }
+      } // no startYml
+      return resource;
+    } else {
+      // Runtime is available - ask it
+      return Runtime.getInstance().getConfig().resource;
+    }
+  }
+}
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index d9b93659cd..53d2019ba7 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -1123,7 +1123,6 @@ public void onStartConfig(String configName) {
    */
   @Override
   public void onStarted(String name) {
-    InMoov2Config c = (InMoov2Config) config;
 
     log.info("onStarted {}", name);
     try {
@@ -1157,81 +1156,21 @@ public void onStarted(String name) {
           chatBot.attachTextListener(getPeerName("htmlFilter"));
           startPeer("htmlFilter");
           break;
-        case "controller3":
-          break;
-        case "controller4":
-          break;
         case "ear":
           AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name);
           ear.attachTextListener(getPeerName("chatBot"));
           break;
-        case "eyeTracking":
-          break;
-        case "fsm":
-          break;
-        case "gpt3":
-          break;
-        case "head":
-          addListener("publishMoveHead", name);
-          break;
-        case "headTracking":
-          break;
         case "htmlFilter":
           TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name);
           htmlFilter.attachTextListener(getPeerName("mouth"));
           break;
-        case "imageDisplay":
-          break;
-        case "leap":
-          break;
-        case "left":
-          break;
-        case "leftArm":
-          addListener("publishMoveLeftArm", name, "onMoveArm");
-          break;
-        case "leftHand":
-          addListener("publishMoveLeftHand", name, "onMoveHand");
-          break;
         case "mouth":
           mouth = (AbstractSpeechSynthesis) Runtime.getService(name);
           mouth.attachSpeechListener(getPeerName("ear"));
           break;
-        case "mouthControl":
-          break;
-        case "neoPixel":
-          break;
         case "opencv":
           subscribeTo(name, "publishOpenCVData");
           break;
-        case "openni":
-          break;
-        case "openWeatherMap":
-          break;
-        case "pid":
-          break;
-        case "pir":
-          break;
-        case "random":
-          break;
-        case "right":
-          break;
-        case "rightArm":
-          addListener("publishMoveRightArm", name, "onMoveArm");
-          break;
-        case "rightHand":
-          addListener("publishMoveRightHand", name, "onMoveHand");
-          break;
-        case "servoMixer":
-          break;
-        case "simulator":
-          break;
-        case "torso":
-          addListener("publishMoveTorso", name);
-          break;
-        case "ultrasonicRight":
-          break;
-        case "ultrasonicLeft":
-          break;
         default:
           log.warn("unknown peer %s not hanled in onStarted", peerKey);
           break;
@@ -1991,28 +1930,6 @@ public void startService() {
     // chatbot getresponse attached to publishEvent
     addListener("publishEvent", getPeerName("chatBot"), "getResponse");
 
-    try {
-      // copy config if it doesn't already exist
-      String resourceBotDir = FileIO.gluePaths(getResourceDir(), "config");
-      List<File> files = FileIO.getFileList(resourceBotDir);
-      for (File f : files) {
-        String botDir = "data/config/" + f.getName();
-        File bDir = new File(botDir);
-        if (bDir.exists() || !f.isDirectory()) {
-          log.info("skipping data/config/{}", botDir);
-        } else {
-          log.info("will copy new data/config/{}", botDir);
-          try {
-            FileIO.copy(f.getAbsolutePath(), botDir);
-          } catch (Exception e) {
-            error(e);
-          }
-        }
-      }
-    } catch (Exception e) {
-      error(e);
-    }
-
     runtime.invoke("publishConfigList");
   }
 
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
index d190af46e8..3b74a3bf20 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
@@ -89,7 +89,7 @@ public static DHRobotArm getDHRobotArm(String name, String side) {
     return arm;
   }
   
-  @Deprecated /* use onMove */
+  @Deprecated /* use onMove(map) */
   public void onMoveArm(HashMap<String, Double> map) {
     onMove(map);
   }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
index 876bfcf5ca..b30c2bd792 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
@@ -490,7 +490,7 @@ public LeapData onLeapData(LeapData data) {
     return data;
   }
 
-  @Deprecated /* use onMove */
+  @Deprecated /* use onMove(map) */
   public void onMoveHand(HashMap<String, Double> map) {
     onMove(map);
   }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java
index f77ef9c823..f3f5edf366 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Head.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java
@@ -221,7 +221,7 @@ public void lookAt(Double x, Double y, Double z) {
     log.info("object distance is {},rothead servo {},neck servo {} ", distance, rotation, colatitude);
   }
 
-  @Deprecated /* use onMoov */
+  @Deprecated /* use onMove(map) */
   public void onMoveHead(HashMap<String, Double> map) {
     onMove(map);
   }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
index f3953699c5..75fa410ca2 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
@@ -94,7 +94,7 @@ public void disable() {
       lowStom.disable();
   }
   
-  @Deprecated /* use onMove */
+  @Deprecated /* use onMove(map) */
   public void onMoveTorso(HashMap<String, Double> map) {
     onMove(map);
   }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 55630c9f1b..c0d624d3e6 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -522,12 +522,12 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishProcessMessage", getPeerName("py4j"), "onPythonMessage"));
 
     // InMoov2 --to--> InMoov2
-    listeners.add(new Listener("publishMoveHead", name));
-    listeners.add(new Listener("publishMoveRightArm", name));
-    listeners.add(new Listener("publishMoveLeftArm", name));
-    listeners.add(new Listener("publishMoveRightHand", name));
-    listeners.add(new Listener("publishMoveLeftHand", name));
-    listeners.add(new Listener("publishMoveTorso", name));
+    listeners.add(new Listener("publishMoveHead", getPeerName("head"), "onMove"));
+    listeners.add(new Listener("publishMoveRightArm", getPeerName("rightArm"), "onMove"));
+    listeners.add(new Listener("publishMoveLeftArm", getPeerName("leftArm"), "onMove"));
+    listeners.add(new Listener("publishMoveRightHand", getPeerName("rightHand"), "onMove"));
+    listeners.add(new Listener("publishMoveLeftHand", getPeerName("leftHand"), "onMove"));
+    listeners.add(new Listener("publishMoveTorso", getPeerName("torso"), "onMove"));
 
     // service --to--> InMoov2
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));

From 135f12aa671f3ede45619cb406c58d7e67f9381f Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 07:20:18 -0800
Subject: [PATCH 022/131] small runtime updates

---
 src/main/java/org/myrobotlab/service/Runtime.java | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 3a66750f54..e5da31305f 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -229,7 +229,7 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
   /**
    * default parent path of configPath static !
    */
-  final static protected String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
+  public final static String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
 
   /**
    * number of services created by this runtime
@@ -5408,4 +5408,12 @@ public static void removeConfig(String configName) {
     }
   }
 
+  /**
+   * Method used to determine is runtime is running without starting it
+   * @return true if available
+   */
+  static public boolean isAvailable() {
+    return runtime != null && runtime.isRunning();
+  }
+
 }

From 13323366c5ef4e1304545fab57945c1af83951e5 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 07:23:58 -0800
Subject: [PATCH 023/131] added test

---
 src/test/java/org/myrobotlab/service/RuntimeTest.java | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java
index 50c6c03268..2a5db617ad 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java
@@ -1,6 +1,7 @@
 package org.myrobotlab.service;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Date;
@@ -103,6 +104,15 @@ public void testRuntimeLocale() {
     assertEquals("fr-FR", l.toString());
 
   }
+  
+  @Test 
+  public void testRuntimeIsAvailable() {
+    Runtime runtime = Runtime.getInstance();
+    assertTrue(Runtime.isAvailable());
+    Runtime.releaseAll(true, true);
+    assertFalse(Runtime.isAvailable());
+  }
+  
 
   @Test
   public void testGetDescribeMessage() {

From 2ea567e0e60b21b3ad343fdc2ee2464e73b408f1 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 07:47:06 -0800
Subject: [PATCH 024/131] trying to make idempotent test

---
 src/test/java/org/myrobotlab/service/RuntimeTest.java | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java
index 2a5db617ad..a20a13db38 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java
@@ -107,10 +107,12 @@ public void testRuntimeLocale() {
   
   @Test 
   public void testRuntimeIsAvailable() {
-    Runtime runtime = Runtime.getInstance();
+    Runtime.getInstance();
     assertTrue(Runtime.isAvailable());
     Runtime.releaseAll(true, true);
     assertFalse(Runtime.isAvailable());
+    Runtime.getInstance();
+    assertTrue(Runtime.isAvailable());
   }
   
 

From 6a0507172abfe51c686c1399ad2626c33b45f408 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 08:32:27 -0800
Subject: [PATCH 025/131] npe check

---
 src/main/java/org/myrobotlab/service/Runtime.java | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 66cf15b322..9def8ddf30 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -2023,7 +2023,9 @@ synchronized public static void unregister(String inName) {
 
     // and config
     RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
-    c.remove(CodecUtils.getShortName(name));
+    if (c != null) {
+      c.remove(CodecUtils.getShortName(name));
+    }
 
     log.info("released {}", name);
   }

From dcdab9bdc4d2e793a148982a21fb4ae8afd9fc41 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 4 Feb 2024 10:57:01 -0800
Subject: [PATCH 026/131] finally ! fixed randomTest issue

---
 src/test/java/org/myrobotlab/service/RandomTest.java | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 9c3739f510..7c8add5923 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -74,7 +74,7 @@ public void testService() throws Exception {
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
-    clock.setInterval(999999);
+    clock.setInterval(9999);
     sleep(200);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
     assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
@@ -83,9 +83,9 @@ public void testService() throws Exception {
     // disable all
     random.disable();
     sleep(200);
-    clock.setInterval(999999);
+    clock.setInterval(9999);
     assertTrue("clock should not be started 4", !clock.isClockRunning());   
-    assertEquals(999999, (long)clock.getInterval());
+    assertEquals(9999, (long)clock.getInterval());
 
     // re-enable all that were previously enabled but not explicitly disabled ones
     random.enable();

From 2e8fdb566d14bb4804a36662ca1fa21ee15d6d8a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 5 Feb 2024 11:14:52 -0800
Subject: [PATCH 027/131] javadoc updates to runtime config

---
 .../java/org/myrobotlab/service/Runtime.java  |  1 -
 .../service/config/RuntimeConfig.java         | 25 +++++++++++++++++--
 2 files changed, 23 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 3f5641ba90..8e96ec71b5 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -938,7 +938,6 @@ public static Runtime getInstance() {
         } catch (Exception e) {
           log.error("runtime will not be loading config", e);
         }
-
       } // synchronized lock
     }
 
diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
index 3e989aaea3..6296f4d536 100644
--- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
@@ -17,12 +17,30 @@ public class RuntimeConfig extends ServiceConfig {
    * virtual hardware if enabled all services created will enable virtualization if applicable
    */
   public Boolean virtual = false;
+  
+  /**
+   * Determines if stdin can be used for commands 
+   */
   public boolean enableCli = true;
+  
+  /**
+   * Log level debug, info, warning, error
+   */
   public String logLevel = "info";
+  
+  /**
+   * Locale setting for the instance, initial default will be set by the default jvm/os
+   * through java.util.Locale.getDefault()
+   */
   public String locale;
   
-  // NEED THIS PRIVATE BUT CANNOT BE
-  public List<String> registry = new ArrayList<>();
+
+  /**
+   * Although this should be a set of unique services, it cannot be a LinkedHashSet
+   * because SnakeYml's interpretation would be a map with null values.  Instead
+   * its a protected member with accessors that prevent duplicates.
+   */
+  protected List<String> registry = new ArrayList<>();
     
   /**
    * Root of resource location
@@ -30,6 +48,9 @@ public class RuntimeConfig extends ServiceConfig {
   public String resource = "resource";
   
   
+  /**
+   * Constructor sets the default locale if not already set.
+   */
   public RuntimeConfig() {
     if (locale == null) {
       locale = Locale.getDefault().getTag();

From 0c4ba80581c9d8bbc0c535c674023a10b9a75507 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 5 Feb 2024 11:36:37 -0800
Subject: [PATCH 028/131] guard against no runtime.xml

---
 src/main/java/org/myrobotlab/service/Runtime.java | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 8e96ec71b5..61f66cd4ef 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -910,7 +910,6 @@ public static Runtime getInstance() {
           Runtime.setAllVirtual(Platform.isVirtual());
 
           // setting the singleton security
-          Security.getInstance();
           runtime.getRepo().addStatusPublisher(runtime);
           FileIO.extractResources();
           // protected services we don't want to remove when releasing a config
@@ -929,7 +928,9 @@ public static Runtime getInstance() {
             } else {
               RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {
               });
-              runtime.apply(rtConfig);
+              if (rtConfig != null) {
+                runtime.apply(rtConfig);
+              }
             }
           } catch (Exception e) {
             log.info("runtime will not be loading config");

From beafe7208cd84d8b0c0bcb26a8ed857fd8241a9b Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 5 Feb 2024 11:59:15 -0800
Subject: [PATCH 029/131] removed dependency test

---
 Jenkinsfile | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/Jenkinsfile b/Jenkinsfile
index 8e7ccc45f2..63a28dcadb 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -64,18 +64,18 @@ pipeline {
         } // stage build
 
    
-      stage('dependencies') {
-         when {
-               expression { params.verify == 'true' }
-         }
-         steps {
-            script {
-                  sh '''
-                     mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
-                  '''
-            }
-         }
-      } // stage dependencies      
+      // stage('dependencies') {
+      //    when {
+      //          expression { params.verify == 'true' }
+      //    }
+      //    steps {
+      //       script {
+      //             sh '''
+      //                mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
+      //             '''
+      //       }
+      //    }
+      // } // stage dependencies      
 
       // --fail-fast
       // -DargLine="-Xmx1024m"

From dbe67cf25dfd86b0a2970bdf567795fd46756a07 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 5 Feb 2024 12:15:48 -0800
Subject: [PATCH 030/131] fixing unit tests

---
 src/main/java/org/myrobotlab/service/Runtime.java      | 10 +++++++++-
 src/test/java/org/myrobotlab/framework/ConfigTest.java |  2 +-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 61f66cd4ef..4976d8c349 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -583,6 +583,8 @@ public final static void createAndStartServices(List<String> services) {
   @Override
   public boolean setVirtual(boolean b) {
     boolean changed = config.virtual != b;
+    config.virtual = b;
+    isVirtual = b;
     setAllVirtual(b);
     if (changed) {
       broadcastState();
@@ -909,7 +911,6 @@ public static Runtime getInstance() {
           // platform virtual is higher priority than service virtual
           Runtime.setAllVirtual(Platform.isVirtual());
 
-          // setting the singleton security
           runtime.getRepo().addStatusPublisher(runtime);
           FileIO.extractResources();
           // protected services we don't want to remove when releasing a config
@@ -932,6 +933,13 @@ public static Runtime getInstance() {
                 runtime.apply(rtConfig);
               }
             }
+            
+
+            // FIXME - should simply set default RuntimeConfig services and include security
+            // setting the singleton security
+            Security.getInstance();
+
+            
           } catch (Exception e) {
             log.info("runtime will not be loading config");
           }
diff --git a/src/test/java/org/myrobotlab/framework/ConfigTest.java b/src/test/java/org/myrobotlab/framework/ConfigTest.java
index 4f8f1a9567..95b3da920b 100644
--- a/src/test/java/org/myrobotlab/framework/ConfigTest.java
+++ b/src/test/java/org/myrobotlab/framework/ConfigTest.java
@@ -109,7 +109,7 @@ public void testStartNoConfig() throws Exception {
     // starting an empty config automatically needs a runtime, and runtime
     // by default starts the singleton security service
     names = Runtime.getServiceNames();
-    assertEquals("complete teardown should be 2 after trying to start a config runtime and security", 2, names.length);
+    assertEquals("complete teardown should be 2 after trying to start a config runtime and security", 1, names.length);
    
   }
   

From 62aa00728e57660ae19e73ea806d78098f3f5d1b Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 6 Feb 2024 06:35:08 -0800
Subject: [PATCH 031/131] config update

---
 .../org/myrobotlab/service/config/OpenWeatherMapConfig.java     | 2 +-
 .../myrobotlab/service/config/YahooFinanceStockQuoteConfig.java | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java b/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
index 37cdd82a0b..9a6a51baab 100644
--- a/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
@@ -3,7 +3,7 @@
 import org.myrobotlab.framework.Peer;
 import org.myrobotlab.framework.Plan;
 
-public class OpenWeatherMapConfig extends ServiceConfig {
+public class OpenWeatherMapConfig extends HttpClientConfig {
 
   public String currentUnits;
   public String currentTown;
diff --git a/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java b/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
index d351154c44..32ae5984d8 100644
--- a/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
@@ -1,5 +1,5 @@
 package org.myrobotlab.service.config;
 
-public class YahooFinanceStockQuoteConfig extends ServiceConfig {
+public class YahooFinanceStockQuoteConfig extends HttpClientConfig {
 
 }

From 139d01f7372467a404a361f57cdd22668923845b Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 6 Feb 2024 06:35:39 -0800
Subject: [PATCH 032/131] http client config

---
 src/main/java/org/myrobotlab/service/HttpClient.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/HttpClient.java b/src/main/java/org/myrobotlab/service/HttpClient.java
index c7caaa2982..bb43043861 100644
--- a/src/main/java/org/myrobotlab/service/HttpClient.java
+++ b/src/main/java/org/myrobotlab/service/HttpClient.java
@@ -54,7 +54,7 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.net.InstallCert;
-import org.myrobotlab.service.config.ServiceConfig;
+import org.myrobotlab.service.config.HttpClientConfig;
 import org.myrobotlab.service.data.HttpData;
 import org.myrobotlab.service.interfaces.HttpDataListener;
 import org.myrobotlab.service.interfaces.HttpResponseListener;
@@ -74,7 +74,7 @@
  *         - Proxies proxies proxies ! -
  *         https://memorynotfound.com/configure-http-proxy-settings-java/
  */
-public class HttpClient<C extends ServiceConfig> extends Service<C> implements TextPublisher {
+public class HttpClient<C extends HttpClientConfig> extends Service<C> implements TextPublisher {
 
   public final static Logger log = LoggerFactory.getLogger(HttpClient.class);
 

From 743226653f5f5f34773837953a9f39b510366bc7 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 9 Feb 2024 10:26:48 -0800
Subject: [PATCH 033/131] synchronized

---
 README.md                                     |   36 +
 .../java/org/myrobotlab/codec/CodecUtils.java |    4 +-
 .../org/myrobotlab/config/ConfigUtils.java    |  142 +-
 .../org/myrobotlab/framework/CmdOptions.java  |   62 +-
 .../org/myrobotlab/framework/Platform.java    |   51 +-
 .../org/myrobotlab/framework/Service.java     |    6 +-
 .../org/myrobotlab/framework/StartYml.java    |    4 -
 .../framework/repo/MavenWrapper.java          |    6 +-
 .../java/org/myrobotlab/process/Launcher.java |   43 +-
 .../java/org/myrobotlab/service/Hd44780.java  |    2 +-
 .../java/org/myrobotlab/service/InMoov2.java  |   42 -
 .../org/myrobotlab/service/JMonkeyEngine.java |    2 +-
 .../org/myrobotlab/service/MotorDualPwm.java  |    2 +-
 .../java/org/myrobotlab/service/Mqtt.java     |    2 +-
 .../java/org/myrobotlab/service/Runtime.java  | 1513 ++++++++---------
 .../java/org/myrobotlab/service/Serial.java   |    2 +-
 .../java/org/myrobotlab/service/WebGui.java   |   46 +-
 .../service/config/RuntimeConfig.java         |   26 +-
 .../service/interfaces/Gateway.java           |    2 +-
 .../myrobotlab/service/meta/JoystickMeta.java |    6 +-
 .../myrobotlab/vertx/WebSocketHandler.java    |    2 +-
 .../org/myrobotlab/codec/CodecUtilsTest.java  |   19 -
 .../myrobotlab/config/ConfigUtilsTest.java    |   43 +
 .../myrobotlab/framework/CmdOptionsTest.java  |   32 +-
 .../org/myrobotlab/framework/ConfigTest.java  |  132 +-
 .../org/myrobotlab/framework/ServiceTest.java |   46 -
 .../org/myrobotlab/service/RuntimeTest.java   |   13 -
 .../org/myrobotlab/service/SerialTest.java    |    2 +-
 .../org/myrobotlab/test/AbstractTest.java     |    8 +-
 29 files changed, 1079 insertions(+), 1217 deletions(-)
 create mode 100644 src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
 delete mode 100644 src/test/java/org/myrobotlab/framework/ServiceTest.java

diff --git a/README.md b/README.md
index 69e1433cbf..f22e3b94f8 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,42 @@ type: Runtime
 virtual: false
 ```
 
+# Starting Flowchart
+```mermaid
+flowchart LR
+    CommandLine[CommandLine]
+    Runtime.main([Runtime.main])
+    install{install}
+    shutdown([shutdown])
+    checkForStartYml{start.yml
+    exists?}
+    startYmlEnabled{start.yml
+    enabled?}
+
+    CommandLine --> Runtime.main
+    Runtime.main --> checkForStartYml
+    checkForStartYml --> |yes| loadStartYml[load start.yml]
+    checkForStartYml --> |no| createDefaultStartYml[create default start.yml]
+    createDefaultStartYml --> loadStartYml
+    loadStartYml --> startYmlEnabled
+    startYmlEnabled --> |yes| Runtime.startConfig[config = start.yml config]
+    startYmlEnabled --> |no| default[config = default]
+    Runtime.startConfig --> loadRuntimeConfig[load runtime config]
+    default --> loadRuntimeConfig
+    loadRuntimeConfig --> startRuntime[start runtime]
+    startRuntime --> applyRuntimeConfig[apply runtime config
+    does not process registry]
+    applyRuntimeConfig --> install{install?}
+
+    install -->|yes| loadServiceData[loadServiceData]
+    install -->|no| Runtime.startConf[get runtime.startConfig config]
+
+    loadServiceData --> findUninstalledDependencies[find uninstallled dependencies]
+    findUninstalledDependencies -->installDependencies[install dependencies]
+    installDependencies --> shutdown
+```
+
+
 # Network Distributed Architecture
 
 ## Websockets - Default Response for New Connection
diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java
index 372caa474b..09d2086c9e 100644
--- a/src/main/java/org/myrobotlab/codec/CodecUtils.java
+++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java
@@ -495,7 +495,7 @@ public static String getFullName(String name) {
     }
 
     if (getId(name) == null) {
-      return name + '@' + Platform.getLocalInstance().getId();
+      return name + '@' + Runtime.getInstance().getId();
     } else {
       return name;
     }
@@ -1466,7 +1466,7 @@ public static boolean isLocal(String name) {
     if (!name.contains("@")) {
       return true;
     }
-    return name.substring(name.indexOf("@") + 1).equals(Platform.getLocalInstance().getId());
+    return name.substring(name.indexOf("@") + 1).equals(Runtime.getInstance().getId());
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 35c8a776a8..19c256a8cf 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -4,13 +4,26 @@
 import java.io.IOException;
 
 import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.framework.CmdOptions;
 import org.myrobotlab.framework.StartYml;
 import org.myrobotlab.io.FileIO;
+import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
 import org.myrobotlab.service.config.RuntimeConfig;
+import org.slf4j.Logger;
 
+/**
+ * Class to process basic configuration functions and processing.
+ * 
+ * @author GroG
+ *
+ */
 public class ConfigUtils {
 
+  public final static Logger log = LoggerFactory.getLogger(Runtime.class);
+
+  private static RuntimeConfig config;
+
   /**
    * This gets the current resource root without starting a Runtime instance if
    * not already started. The resource root depends on config, if Runtime is
@@ -21,51 +34,100 @@ public class ConfigUtils {
    * @return
    */
   public static String getResourceRoot() {
+    if (config == null) {
+      loadRuntimeConfig(null);
+    }
+    return config.resource;
+
+  }
+
+  /**
+   * Loads a runtime config based on the configName. config =
+   * data/config/{configName}/runtime.yml If one does exits, it is returned, if
+   * one does not exist a default one is created and saved.
+   * 
+   * @param configName
+   * @return
+   */
+  static public RuntimeConfig loadRuntimeConfig(CmdOptions options) {
+
+    if (config != null) {
+      return config;
+    }
 
-    String resource = "resource";
+    StartYml startYml = loadStartYml();
+    String configName = null;
 
-    // check if runtime is running
-    if (!Runtime.isAvailable()) {
-      // check for start.yml
+    if (startYml.enable) {
+      configName = startYml.config;
+    }
+
+    // start with default
+    config = new RuntimeConfig();
+    try {
 
-      File checkStartYml = new File("start.yml");
-      StartYml startYml = new StartYml();
-      if (checkStartYml.exists()) {
-        String yml;
+      File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + configName + File.separator + "runtime.yml");
+      if (runtimeYml.exists()) {
+        // parse that file look for resource: entry in file
+        config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+      } else {
+        FileIO.toFile(runtimeYml, CodecUtils.toYaml(config).getBytes());
+      }
+
+    } catch (IOException e) {
+      log.error("loadRuntimeConfig threw", e);
+    }
+
+    if (options != null && options.id != null) {
+      config.id = options.id;
+    }
+
+    return config;
+  }
+
+  public static StartYml loadStartYml() {
+    StartYml startYml = new StartYml();
+    String defaultStartFile = CodecUtils.toYaml(startYml);
+    File checkStartYml = new File("start.yml");
+    if (!checkStartYml.exists()) {
+      // save default start.yml
+      startYml = new StartYml();
+      try {
+        FileIO.toFile("start.yml", defaultStartFile);
+      } catch (IOException e) {
+        log.error("could not save start.yml");
+      }
+    } else {
+      // load start.yml
+      try {
+        String yml = FileIO.toString("start.yml");
+        startYml = CodecUtils.fromYaml(yml, StartYml.class);
+      } catch (Exception e) {
+        log.error("could not load start.yml replacing with new start.yml", e);
+        startYml = new StartYml();
         try {
-          yml = FileIO.toString("start.yml");
-          startYml = CodecUtils.fromYaml(yml, StartYml.class);
-
-          // see if autostart is on with a config
-          if (startYml.enable) {
-            // use that config to find runtime.yml
-
-            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + startYml.config + File.separator + "runtime.yml");
-            if (runtimeYml.exists()) {
-              // parse that file look for resource: entry in file
-              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
-              resource = config.resource;
-            }
-
-          } else {
-            // start.yml enable = false / so we'll use default config
-            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + "default" + File.separator + "runtime.yml");
-            if (runtimeYml.exists()) {
-              // parse that file look for resource: entry in file
-              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
-              resource = config.resource;
-            }
-          }
-
-        } catch (IOException e) {
-          // problem getting or parsing
-          // going to assume default "resource"
+          FileIO.toFile("start.yml", defaultStartFile);
+        } catch (IOException ex) {
+          log.error("could not save start.yml", ex);
         }
-      } // no startYml
-      return resource;
-    } else {
-      // Runtime is available - ask it
-      return Runtime.getInstance().getConfig().resource;
+      }
     }
+    log.info("start.yml exists {} {}", checkStartYml.exists(), CodecUtils.toJson(startYml));
+    return startYml;
   }
+
+  public static String getId() {
+    if (config == null) {
+      loadRuntimeConfig(null);
+    }
+    return config.id;
+  }
+
+  /**
+   * If Runtime.releaseAll is called the statics here should be reset
+   */
+  public static void reset() {
+    config = null;
+  }
+
 }
diff --git a/src/main/java/org/myrobotlab/framework/CmdOptions.java b/src/main/java/org/myrobotlab/framework/CmdOptions.java
index f0eb00c0e7..2c357e8db6 100644
--- a/src/main/java/org/myrobotlab/framework/CmdOptions.java
+++ b/src/main/java/org/myrobotlab/framework/CmdOptions.java
@@ -26,9 +26,7 @@
  * </pre>
  */
 @Command(name = "java -jar myrobotlab.jar ")
-public class CmdOptions {  
-
-  public final String DEFAULT_CONNECT = "http://localhost:8888";
+public class CmdOptions {
 
   static boolean contains(List<String> l, String flag) {
     for (String f : l) {
@@ -39,51 +37,28 @@ static boolean contains(List<String> l, String flag) {
     return false;
   }
 
-  // launcher ??
-  @Option(names = { "-a", "--auto-update" }, description = "auto updating - this feature allows mrl instances to be automatically updated when a new version is available")
-  public boolean autoUpdate = false;
-
   // launcher
   @Option(names = { "-c",
-      "--config" }, fallbackValue="default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
+      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
   public String config = null;
 
-  @Option(names = {
-      "--connect" }, arity = "0..*", /*
-                                      * defaultValue = DEFAULT_CONNECT,
-                                      */ fallbackValue = DEFAULT_CONNECT, description = "connects this mrl instance to another mrl instance - default is " + DEFAULT_CONNECT)
-  public String connect = null;
-
   @Option(names = { "-h", "-?", "--help" }, description = "shows help")
   public boolean help = false;
-  
-  @Option(names = { "-r", "--config-root" }, description = "sets configuration root, the root for which all config directories are in")
-  public String configRoot = null;
-
-
-  @Option(names = { "--id" }, description = "process identifier to be mdns or network overlay name for this instance - one is created at random if not assigned")
+  @Option(names = {
+      "--id" }, description = "process identifier to be mdns or network overlay name for this instance - one is created at random if not assigned")
   public String id;
 
   @Option(names = { "-i",
       "--install" }, arity = "0..*", description = "installs all dependencies for all services, --install {serviceType} installs dependencies for a specific service, if no type is specified then all services are installed")
   public String install[];
 
-  @Option(names = { "-I",
-      "--invoke" }, arity = "0..*", description = "invokes a method on a service --invoke {serviceName} {method} {param0} {param1} ... : --invoke python execFile myFile.py")
-  public String invoke[];
-
-  // for launcher
   @Option(names = { "-j", "--jvm" }, arity = "0..*", description = "jvm parameters for the instance of mrl")
   public String jvm;
 
-  @Option(names = { "-l", "--log-level" }, description = "log level - helpful for troubleshooting [debug info warn error]")
+  @Option(names = { "-l",
+      "--log-level" }, description = "log level - helpful for troubleshooting [debug info warn error]")
   public String logLevel = "info";
 
-  @Option(names = { "--log-file" }, description = "log file name [myrobotlab.log]")
-  public String logFile = "myrobotlab.log";
-
-  // FIXME - highlight or italics for examples !!
-  // launcher
   @Option(names = { "-m", "--memory" }, description = "adjust memory can e.g. -m 2g \n -m 128m")
   public String memory = null;
 
@@ -91,9 +66,6 @@ static boolean contains(List<String> l, String flag) {
       "--services" }, arity = "0..*", description = "services requested on startup, the services must be {name} {Type} paired, e.g. gui SwingGui webgui WebGui servo Servo ...")
   public List<String> services = new ArrayList<>();
 
-  @Option(names = { "-V", "--virtual" }, description = "sets global environment as virtual - all services which support virtual hardware will create virtual hardware")
-  public boolean virtual = false;
-
   public CmdOptions() {
   }
 
@@ -133,34 +105,18 @@ public static String toString(String[] cmdLine) {
    * 
    * @return the list of output command
    * @throws IOException
-   *           boom
+   *                     boom
    * 
    */
   public List<String> getOutputCmd() throws IOException {
 
     List<String> cmd = new ArrayList<>();
 
-    if (autoUpdate) {
-      cmd.add("-a");
-    }
-
     if (config != null) {
       cmd.add("--config");
       cmd.add(config);
     }
 
-    if (connect != null) {
-      cmd.add("-c");
-      cmd.add(connect);
-    }
-
-    if (invoke != null) {
-      cmd.add("-I");
-      for (int i = 0; i < invoke.length; ++i) {
-        cmd.add(invoke[i]);
-      }
-    }
-
     if (help) {
       cmd.add("-h");
     }
@@ -206,10 +162,6 @@ public List<String> getOutputCmd() throws IOException {
       cmd.add(s);
     }
 
-    if (virtual) {
-      cmd.add("-v");
-    }
-
     return cmd;
   }
 
diff --git a/src/main/java/org/myrobotlab/framework/Platform.java b/src/main/java/org/myrobotlab/framework/Platform.java
index 1b1ed4f2d5..5742bf365e 100644
--- a/src/main/java/org/myrobotlab/framework/Platform.java
+++ b/src/main/java/org/myrobotlab/framework/Platform.java
@@ -13,6 +13,7 @@
 import java.util.TreeMap;
 import java.util.zip.ZipFile;
 
+import org.myrobotlab.config.ConfigUtils;
 // Do not pull in deps to this class !
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.logging.Level;
@@ -64,13 +65,7 @@ public class Platform implements Serializable {
   String vmName;
   String vmVersion;
   String mrlVersion;
-  boolean isVirtual = false;
 
-  /**
-   * Static identifier to identify the "instance" of myrobotlab - similar to
-   * network ip of a device and used in a similar way
-   */
-  String id;
   String branch;
 
   String pid;
@@ -95,7 +90,7 @@ public class Platform implements Serializable {
    * All data should be accessed through public functions on the local instance.
    * If the local instance is desired. If its from a serialized instance, the
    * "getters" will be retrieving appropriate info for that serialized instance.
-   * 
+   *  
    * @return - return the local instance of the current platform
    */
   public static Platform getLocalInstance() {
@@ -121,7 +116,8 @@ public static Platform getLocalInstance() {
 
       // === ARCH ===
       String arch = System.getProperty("os.arch").toLowerCase();
-      if ("i386".equals(arch) || "i486".equals(arch) || "i586".equals(arch) || "i686".equals(arch) || "amd64".equals(arch) || arch.startsWith("x86")) {
+      if ("i386".equals(arch) || "i486".equals(arch) || "i586".equals(arch) || "i686".equals(arch)
+          || "amd64".equals(arch) || arch.startsWith("x86")) {
         platform.arch = "x86"; // don't care at the moment
       }
 
@@ -159,7 +155,8 @@ public static Platform getLocalInstance() {
         // tries very hard to hide this from running programs
         String procArch = System.getenv("PROCESSOR_ARCHITECTURE");
         String procArchWow64 = System.getenv("PROCESSOR_ARCHITEW6432");
-        platform.osBitness = (procArch != null && procArch.endsWith("64") || procArchWow64 != null && procArchWow64.endsWith("64")) ? 64 : 32;
+        platform.osBitness = (procArch != null && procArch.endsWith("64")
+            || procArchWow64 != null && procArchWow64.endsWith("64")) ? 64 : 32;
         switch (arch) {
           case "x86":
           case "i386":
@@ -460,19 +457,6 @@ public String toString() {
     return String.format("%s.%d.%s", arch, jvmBitness, os);
   }
 
-  /**
-   * @return The instance identifier of the current running myrobotlab. Used for
-   *         connecting multiple myrobotlabs together
-   * 
-   */
-  public String getId() {
-    // null ids are not allowed
-    if (id == null) {
-      id = NameGenerator.getName();
-    }
-    return id;
-  }
-
   /**
    * @return The Computer's hostname
    */
@@ -480,15 +464,6 @@ public String getHostname() {
     return hostname;
   }
 
-  /**
-   * @param newId
-   *          Set your own instance identifier
-   * 
-   */
-  public void setId(String newId) {
-    id = newId;
-  }
-
   /**
    * @return the time when this instance was started
    * 
@@ -497,20 +472,6 @@ public Date getStartTime() {
     return startTime;
   }
 
-  /**
-   * @return true if running in virtual mode
-   * 
-   */
-  public static boolean isVirtual() {
-    Platform p = getLocalInstance();
-    return p.isVirtual;
-  }
-
-  public static void setVirtual(boolean b) {
-    Platform p = getLocalInstance();
-    p.isVirtual = b;
-  }
-
   public static void main(String[] args) {
     try {
       LoggingFactory.init(Level.DEBUG);
diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 95e23a5616..0449c05e16 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -619,7 +619,7 @@ public Service(String reservedKey, String inId) {
 
     // necessary for serialized transport\
     if (inId == null) {
-      id = Platform.getLocalInstance().getId();
+      id = ConfigUtils.getId();
       log.debug("creating local service for id {}", id);
     } else {
       id = inId;
@@ -670,7 +670,7 @@ public Service(String reservedKey, String inId) {
     // register this service if local - if we are a foreign service, we probably
     // are being created in a
     // registration already
-    if (id.equals(Platform.getLocalInstance().getId())) {
+    if (id.equals(ConfigUtils.getId())) {
       Registration registration = new Registration(this);
       Runtime.register(registration);
     }
@@ -1504,7 +1504,7 @@ public ServiceConfig getFilteredConfig() {
           // The StringUtils.removeEnd() call is a no-op when the ID is not our
           // local ID,
           // so doesn't conflict with remote routes
-          Listener newConfigListener = new Listener(listener.topicMethod, StringUtil.removeEnd(listener.callbackName, '@' + Platform.getLocalInstance().getId()),
+          Listener newConfigListener = new Listener(listener.topicMethod, StringUtil.removeEnd(listener.callbackName, '@' + Runtime.getInstance().getId()),
               listener.callbackMethod);
           newListeners.add(newConfigListener);
         }
diff --git a/src/main/java/org/myrobotlab/framework/StartYml.java b/src/main/java/org/myrobotlab/framework/StartYml.java
index c8bfb25a44..b1806d203c 100644
--- a/src/main/java/org/myrobotlab/framework/StartYml.java
+++ b/src/main/java/org/myrobotlab/framework/StartYml.java
@@ -9,10 +9,6 @@
  *
  */
 public class StartYml {
-  /**
-   * instance id of myrobotlab, default will be dynamically generated
-   */
-  public String id;  
     
   /**
    * configuration set to start under /data/config/{configName}
diff --git a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
index 8374f92cdf..62c0027cbd 100644
--- a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
+++ b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
@@ -1,5 +1,5 @@
 package org.myrobotlab.framework.repo;
-
+import org.myrobotlab.service.Runtime;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -281,6 +281,8 @@ public static void main(String[] args) {
 
       LoggingFactory.init(Level.INFO);
       
+      Runtime.getInstance();
+      
       File libraries = new File(ServiceData.LIBRARIES);
       libraries.mkdir();
       File cache = new File(ServiceData.LIBRARIES + File.separator + "serviceData.json");
@@ -309,7 +311,7 @@ public static void main(String[] args) {
       // repo.installTo(dir);
       // repo.install();
       // repo.installEach(); <-- TODO - test
-
+      Runtime.shutdown();
       log.info("done");
 
     } catch (Exception e) {
diff --git a/src/main/java/org/myrobotlab/process/Launcher.java b/src/main/java/org/myrobotlab/process/Launcher.java
index 3e819f3c59..7954fd7d5d 100644
--- a/src/main/java/org/myrobotlab/process/Launcher.java
+++ b/src/main/java/org/myrobotlab/process/Launcher.java
@@ -2,9 +2,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -241,41 +238,15 @@ public static void main(String[] args) {
         return;
       }
 
-      boolean instanceAlreadyRunning = false;
-
-      try {
-        URI uri = new URI(options.connect);
-        Socket socket = new Socket();
-        socket.connect(new InetSocketAddress(uri.getHost(), uri.getPort()), 1000);
-        socket.close();
-        instanceAlreadyRunning = true;
-      } catch (Exception e) {
-        log.info("could not connect to {}", options.connect);
+      log.info("spawning new instance");
+      ProcessBuilder builder = createBuilder(options);
+      process = builder.start();
+      if (process.isAlive()) {
+        log.info("process is alive");
+      } else {
+        log.error("process died");
       }
 
-      if (instanceAlreadyRunning && options.connect.equals(options.DEFAULT_CONNECT)) {
-        log.error("zombie instance already running at {}", options.DEFAULT_CONNECT);
-        return;
-      }
-
-      if (!instanceAlreadyRunning || !options.connect.equals(options.DEFAULT_CONNECT)) {
-        log.info("spawning new instance");
-        ProcessBuilder builder = createBuilder(options);
-        process = builder.start();
-        if (process.isAlive()) {
-          log.info("process is alive");
-        } else {
-          log.error("process died");
-        }
-      }
-
-      /*
-       * // FIXME - use wsclient for remote access if (options.client != null) {
-       * // FIXME - delay & auto connect Client.main(new String[] { "-c",
-       * options.client }); } else { // terminating - "if" runtime exists - if
-       * not no biggy Runtime.shutdown(); }
-       */
-
     } catch (Exception e) {
       log.error("main threw", e);
     }
diff --git a/src/main/java/org/myrobotlab/service/Hd44780.java b/src/main/java/org/myrobotlab/service/Hd44780.java
index 9294fee992..72a4039585 100644
--- a/src/main/java/org/myrobotlab/service/Hd44780.java
+++ b/src/main/java/org/myrobotlab/service/Hd44780.java
@@ -687,7 +687,7 @@ public void preShutdown() {
   public static void main(String[] args) {
     try {
       LoggingFactory.init(Level.INFO);
-      Platform.setVirtual(false);
+      Runtime.getInstance().setVirtual(false);
 
       Runtime.start("webgui", "WebGui");
       Pcf8574 pcf = (Pcf8574) Runtime.start("pcf8574t", "Pcf8574");
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 0c4486c40c..e6ad6326ab 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -2178,7 +2178,6 @@ public void startAll() throws Exception {
 
   @Deprecated /* use startPeers */
   public void startAll(String leftPort, String rightPort) throws Exception {
-    startMouth();
     startChatBot();
 
     // startHeadTracking();
@@ -2289,47 +2288,6 @@ public void startHeartbeat() {
     heart.start();
   }
 
-  // TODO - general objective "might" be to reduce peers down to something
-  // that does not need a reference - where type can be switched before creation
-  // and the only thing needed is pubs/subs that are not handled in abstracts
-  @Deprecated /* use startPeer */
-  public SpeechSynthesis startMouth() {
-
-    // FIXME - set type ??? - maybe a good product of InMoov
-    // if "new" type cannot necessarily grab yml file
-    // setMouthType
-
-    // FIXME - bad to have a reference, should only need the "name" of the
-    // service !!!
-    mouth = (SpeechSynthesis) startPeer("mouth");
-
-    // voices = mouth.getVoices();
-    // Voice voice = mouth.getVoice();
-    // if (voice != null) {
-    // voiceSelected = voice.getName();
-    // }
-
-    if (mute) {
-      mouth.setMute(true);
-    }
-
-    mouth.attachSpeechRecognizer(ear);
-    // mouth.attach(htmlFilter); // same as chatBot not needed
-
-    // this.attach((Attachable) mouth);
-    // if (ear != null) ....
-
-    broadcastState();
-
-    speakBlocking(get("STARTINGMOUTH"));
-    if (Platform.isVirtual()) {
-      speakBlocking(get("STARTINGVIRTUALHARD"));
-    }
-    speakBlocking(get("WHATISTHISLANGUAGE"));
-
-    return mouth;
-  }
-
   @Deprecated /* use startPeer */
   public OpenCV startOpenCV() {
     speakBlocking(get("STARTINGOPENCV"));
diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
index 2d19048c84..af2d354f43 100644
--- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
+++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
@@ -2489,7 +2489,7 @@ public static void main(String[] args) {
         i01.startPeer("simulator");
       }
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
       // Runtime.main(new String[] { "--interactive", "--id", "admin" });
       JMonkeyEngine jme = (JMonkeyEngine) Runtime.start("simulator", "JMonkeyEngine");
 
diff --git a/src/main/java/org/myrobotlab/service/MotorDualPwm.java b/src/main/java/org/myrobotlab/service/MotorDualPwm.java
index e1746ca071..3187e52ee0 100644
--- a/src/main/java/org/myrobotlab/service/MotorDualPwm.java
+++ b/src/main/java/org/myrobotlab/service/MotorDualPwm.java
@@ -96,7 +96,7 @@ public static void main(String[] args) {
       LoggingFactory.init(Level.INFO);
       String arduinoPort = "COM5";
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
       Runtime.startConfig("dev");
       Runtime.start("webgui", "WebGui");
       MotorDualPwm motor = (MotorDualPwm) Runtime.start("motor", "MotorDualPwm");
diff --git a/src/main/java/org/myrobotlab/service/Mqtt.java b/src/main/java/org/myrobotlab/service/Mqtt.java
index 8466016039..b8e27bcc38 100644
--- a/src/main/java/org/myrobotlab/service/Mqtt.java
+++ b/src/main/java/org/myrobotlab/service/Mqtt.java
@@ -570,7 +570,7 @@ public void messageArrived(String topic, MqttMessage message) throws MqttExcepti
           // 4. describe new instance for me
           // FIXME why isn't this using Gateway.getDescribeMessage()?
           Message describe = Message.createMessage(String.format("%s@%s", getName(), getId()), "runtime@" + remoteId, "describe",
-              new Object[] { Gateway.FILL_UUID_MAGIC_VAL, new DescribeQuery(Platform.getLocalInstance().getId(), uuid) });
+              new Object[] { Gateway.FILL_UUID_MAGIC_VAL, new DescribeQuery(Runtime.getInstance().getId(), uuid) });
           describe.sendingMethod = "onConnect";
           sendRemote(describe);
 
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 4976d8c349..36fe72a3f8 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -47,6 +47,7 @@
 import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.codec.CodecUtils.ApiDescription;
 import org.myrobotlab.codec.ForeignProcessUtils;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.CmdOptions;
 import org.myrobotlab.framework.DescribeQuery;
 import org.myrobotlab.framework.DescribeResults;
@@ -129,7 +130,7 @@
  *
  */
 public class Runtime extends Service<RuntimeConfig> implements MessageListener, ServiceLifeCyclePublisher, RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider {
-  
+
   final static private long serialVersionUID = 1L;
 
   // FIXME - AVOID STATIC FIELDS !!! use .getInstance() to get the singleton
@@ -167,6 +168,8 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
 
   protected final Map<String, Set<String>> typeToInterface = new HashMap<>();
 
+  private static final Object processLock = new Object();
+
   /**
    * FILTERED_INTERFACES are the set of low level interfaces which we are
    * interested in filtering out if we want to maintain a data structure which
@@ -188,7 +191,13 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
    * name. It cannot be null, it cannot have "/" or "\" in the name - it has to
    * be a valid file name for the OS. It's defaulted to "default". Changed often
    */
-  protected String configName = "default";
+  protected static String configName = "default";
+
+  /**
+   * The runtime config which Runtime was started with. This is the config which
+   * will be applied to Runtime when its created on startup.
+   */
+  // protected static RuntimeConfig startConfig = null;
 
   /**
    * State variable reporting if runtime is currently starting services from
@@ -378,30 +387,32 @@ static public ServiceInterface create(String name) {
    *          - Can be null if a service file exists for named service
    * @return the service
    */
-  static public synchronized ServiceInterface create(String name, String type) {
+  static public ServiceInterface create(String name, String type) {
 
-    try {
-      ServiceInterface si = Runtime.getService(name);
-      if (si != null) {
-        return si;
-      }
+    synchronized (processLock) {
 
-      // FIXME remove configName from loadService
-      Plan plan = Runtime.load(name, type);
-      Runtime.check(name, type);
-      // at this point - the plan should be loaded, now its time to create the
-      // children peers
-      // and parent service
-      createServicesFromPlan(plan, null, name);
-      si = Runtime.getService(name);
-      if (si == null) {
-        Runtime.getInstance().error("coult not create %s of type %s", name, type);
+      try {
+        ServiceInterface si = Runtime.getService(name);
+        if (si != null) {
+          return si;
+        }
+
+        Plan plan = Runtime.load(name, type);
+        Runtime.check(name, type);
+        // at this point - the plan should be loaded, now its time to create the
+        // children peers
+        // and parent service
+        createServicesFromPlan(plan, null, name);
+        si = Runtime.getService(name);
+        if (si == null) {
+          Runtime.getInstance().error("coult not create %s of type %s", name, type);
+        }
+        return si;
+      } catch (Exception e) {
+        runtime.error(e);
       }
-      return si;
-    } catch (Exception e) {
-      runtime.error(e);
+      return null;
     }
-    return null;
   }
 
   /**
@@ -414,43 +425,46 @@ static public synchronized ServiceInterface create(String name, String type) {
    * @param name
    * @return
    */
-  synchronized private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, Map<String, ServiceInterface> createdServices, String name) {
-
-    if (createdServices == null) {
-      createdServices = new LinkedHashMap<>();
-    }
+  private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, Map<String, ServiceInterface> createdServices, String name) {
 
-    // Plan's config
-    RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
-    // current Runtime config
-    RuntimeConfig currentConfig = Runtime.getInstance().config;
+    synchronized (processLock) {
 
-    for (String service : plansRtConfig.getRegistry()) {
-      // FIXME - determine if you want to return a complete merge of activated
-      // or just "recent"
-      if (Runtime.getService(service) != null) {
-        continue;
+      if (createdServices == null) {
+        createdServices = new LinkedHashMap<>();
       }
-      ServiceConfig sc = plan.get(service);
-      if (sc == null) {
-        runtime.error("could not get %s from plan", service);
-        continue;
-      }
-      ServiceInterface si = createService(service, sc.type, null);
-      // process the base listeners/subscription of ServiceConfig
-      si.addConfigListeners(sc);
-      if (si instanceof ConfigurableService) {
-        try {
-          ((ConfigurableService) si).apply(sc);
-        } catch (Exception e) {
-          Runtime.getInstance().error("could not apply config of type %s to service %s, using default config", sc.type, si.getName(), sc.type);
+
+      // Plan's config
+      RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
+      // current Runtime config
+      RuntimeConfig currentConfig = Runtime.getInstance().config;
+
+      for (String service : plansRtConfig.getRegistry()) {
+        // FIXME - determine if you want to return a complete merge of activated
+        // or just "recent"
+        if (Runtime.getService(service) != null) {
+          continue;
+        }
+        ServiceConfig sc = plan.get(service);
+        if (sc == null) {
+          runtime.error("could not get %s from plan", service);
+          continue;
+        }
+        ServiceInterface si = createService(service, sc.type, null);
+        // process the base listeners/subscription of ServiceConfig
+        si.addConfigListeners(sc);
+        if (si instanceof ConfigurableService) {
+          try {
+            ((ConfigurableService) si).apply(sc);
+          } catch (Exception e) {
+            Runtime.getInstance().error("could not apply config of type %s to service %s, using default config", sc.type, si.getName(), sc.type);
+          }
         }
+        createdServices.put(service, si);
+        currentConfig.add(service);
       }
-      createdServices.put(service, si);
-      currentConfig.add(service);
-    }
 
-    return createdServices;
+      return createdServices;
+    }
   }
 
   public String getServiceExample(String serviceType) {
@@ -601,7 +615,6 @@ public boolean setVirtual(boolean b) {
    * @return b
    */
   static public boolean setAllVirtual(boolean b) {
-    Platform.setVirtual(b);
     for (ServiceInterface si : getServices()) {
       if (!si.isRuntime()) {
         si.setVirtual(b);
@@ -624,7 +637,6 @@ static public boolean setAllVirtual(boolean b) {
    */
   public void setAutoStart(boolean autoStart) throws IOException {
     log.debug("setAutoStart {}", autoStart);
-    startYml.id = getId();
     startYml.enable = autoStart;
     startYml.config = configName;
     FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
@@ -659,126 +671,128 @@ public void setAutoStart(boolean autoStart) throws IOException {
    *         '/', or a service with the same name exists but has a different
    *         type, will return null instead.
    */
-  static private synchronized ServiceInterface createService(String name, String type, String inId) {
-    log.info("Runtime.createService {}", name);
+  static private ServiceInterface createService(String name, String type, String inId) {
+    synchronized (processLock) {
+      log.info("Runtime.createService {}", name);
 
-    if (name == null) {
-      runtime.error("service name cannot be null");
+      if (name == null) {
+        runtime.error("service name cannot be null");
 
-      return null;
-    }
+        return null;
+      }
 
-    if (name.contains("@") || name.contains("/")) {
-      runtime.error("service name cannot contain '@' or '/': {}", name);
+      if (name.contains("@") || name.contains("/")) {
+        runtime.error("service name cannot contain '@' or '/': {}", name);
 
-      return null;
-    }
+        return null;
+      }
 
-    String fullName;
-    if (inId == null || inId.equals(""))
-      fullName = getFullName(name);
-    else
-      fullName = String.format("%s@%s", name, inId);
+      String fullName;
+      if (inId == null || inId.equals(""))
+        fullName = getFullName(name);
+      else
+        fullName = String.format("%s@%s", name, inId);
 
-    if (type == null) {
-      ServiceConfig sc;
-      try {
-        sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
-      } catch (IOException e) {
-        runtime.error("could not find type for service %s", name);
-        return null;
+      if (type == null) {
+        ServiceConfig sc;
+        try {
+          sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
+        } catch (IOException e) {
+          runtime.error("could not find type for service %s", name);
+          return null;
+        }
+        if (sc != null) {
+          log.info("found type for {} in plan", name);
+          type = sc.type;
+        } else {
+          runtime.error("createService type not specified and could not get type for {} from plan", name);
+          return null;
+        }
       }
-      if (sc != null) {
-        log.info("found type for {} in plan", name);
-        type = sc.type;
-      } else {
-        runtime.error("createService type not specified and could not get type for {} from plan", name);
+
+      if (type == null) {
+        runtime.error("cannot create service {} no type in plan or yml file", name);
         return null;
       }
-    }
 
-    if (type == null) {
-      runtime.error("cannot create service {} no type in plan or yml file", name);
-      return null;
-    }
+      String fullTypeName = CodecUtils.makeFullTypeName(type);
+
+      ServiceInterface si = Runtime.getService(fullName);
+      if (si != null) {
+        if (!si.getTypeKey().equals(fullTypeName)) {
+          runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type);
+          return null;
+        }
+        return si;
+      }
 
-    String fullTypeName = CodecUtils.makeFullTypeName(type);
+      // DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
+      // only try to resolve type by the plan - if not then error out
 
-    ServiceInterface si = Runtime.getService(fullName);
-    if (si != null) {
-      if (!si.getTypeKey().equals(fullTypeName)) {
-        runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type);
+      String id = (inId == null) ? Runtime.getInstance().getId() : inId;
+      if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
+        log.error("{} not a type or {} not defined ", fullTypeName, name);
         return null;
       }
-      return si;
-    }
-
-    // DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
-    // only try to resolve type by the plan - if not then error out
 
-    String id = (inId == null) ? Platform.getLocalInstance().getId() : inId;
-    if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
-      log.error("{} not a type or {} not defined ", fullTypeName, name);
-      return null;
-    }
+      // TODO - test new create of existing service
+      ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
+      if (sw != null) {
+        log.info("service {} already exists", name);
+        return sw;
+      }
 
-    // TODO - test new create of existing service
-    ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
-    if (sw != null) {
-      log.info("service {} already exists", name);
-      return sw;
-    }
+      try {
 
-    try {
+        if (log.isDebugEnabled()) {
+          // TODO - determine if there have been new classes added from
+          // ivy --> Boot Classloader --> Ext ClassLoader --> System
+          // ClassLoader
+          // http://blog.jamesdbloom.com/JVMInternals.html
+          log.debug("ABOUT TO LOAD CLASS");
+          log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
+          log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("system class loader " + ClassLoader.getSystemClassLoader());
+          log.debug("parent should be null" + ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("thread context " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
+          log.debug("thread context parent " + Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
+        }
 
-      if (log.isDebugEnabled()) {
-        // TODO - determine if there have been new classes added from
-        // ivy --> Boot Classloader --> Ext ClassLoader --> System
-        // ClassLoader
-        // http://blog.jamesdbloom.com/JVMInternals.html
-        log.debug("ABOUT TO LOAD CLASS");
-        log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
-        log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
-        log.debug("system class loader " + ClassLoader.getSystemClassLoader());
-        log.debug("parent should be null" + ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
-        log.debug("thread context " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
-        log.debug("thread context parent " + Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
-      }
+        // FIXME - error if deps are missing - prompt license
+        // require restart !
+        // FIXME - this should happen after inspecting the "loaded" "plan" not
+        // during the create/start/apply !
+
+        // create an instance
+        Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
+        log.debug("returning {}", fullTypeName);
+        si = (ServiceInterface) newService;
+
+        // si.setId(id);
+        if (Runtime.getInstance().getId().equals(id)) {
+          si.setVirtual(Runtime.getInstance().isVirtual());
+          Runtime.getInstance().creationCount++;
+          si.setOrder(Runtime.getInstance().creationCount);
+        }
 
-      // FIXME - error if deps are missing - prompt license
-      // require restart !
-      // FIXME - this should happen after inspecting the "loaded" "plan" not
-      // during the create/start/apply !
-
-      // create an instance
-      Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
-      log.debug("returning {}", fullTypeName);
-      si = (ServiceInterface) newService;
-
-      // si.setId(id);
-      if (Platform.getLocalInstance().getId().equals(id)) {
-        si.setVirtual(Platform.isVirtual());
-        Runtime.getInstance().creationCount++;
-        si.setOrder(Runtime.getInstance().creationCount);
-      }
+        if (runtime != null) {
 
-      if (runtime != null) {
+          runtime.invoke("created", getFullName(name));
 
-        runtime.invoke("created", getFullName(name));
+          // add all the service life cycle subscriptions
+          // runtime.addListener("registered", name);
+          // runtime.addListener("created", name);
+          // runtime.addListener("started", name);
+          // runtime.addListener("stopped", name);
+          // runtime.addListener("released", name);
+        }
 
-        // add all the service life cycle subscriptions
-        // runtime.addListener("registered", name);
-        // runtime.addListener("created", name);
-        // runtime.addListener("started", name);
-        // runtime.addListener("stopped", name);
-        // runtime.addListener("released", name);
+        return (Service) newService;
+      } catch (Exception e) {
+        log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
       }
-
-      return (Service) newService;
-    } catch (Exception e) {
-      log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
+      return null;
     }
-    return null;
   }
 
   static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries() {
@@ -885,67 +899,47 @@ public static final long getFreeMemory() {
   public static Runtime getInstance() {
     if (runtime == null) {
       synchronized (INSTANCE_LOCK) {
-        if (runtime == null) {
+        try {
 
-          // all though this is appropriate it cannot be done
-          // because you need runtime to correctly load/start/etc the plan
-          // so it needs to be bootstrapped
-          // load("runtime", "Runtime");
+          RuntimeConfig c = null;
+          if (runtime == null) {
+            c = ConfigUtils.loadRuntimeConfig(options);
 
-          // just create Runtime
-          runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", Platform.getLocalInstance().getId());
-        }
-        try {
-          // a bit backwards - it loads after it been created
-          // but its necessary because you need an runtime instance before you
-          // load
-
-          File cfgRoot = new File(ROOT_CONFIG_DIR);
-          cfgRoot.mkdirs();
-          if (startYml.enable) {
-            Runtime.load("runtime", "Runtime");
-          }
-          runtime.config.add("runtime");
+            runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", c.id);
+            runtime.startService();
+            // klunky
+            Runtime.register(new Registration(runtime));
 
-          runtime.startService();
-          // platform virtual is higher priority than service virtual
-          Runtime.setAllVirtual(Platform.isVirtual());
+            // assign, do not apply otherwise there will be
+            // a chicken-egg problem
+            runtime.config = c;
+          }
 
           runtime.getRepo().addStatusPublisher(runtime);
+          runtime.startService();
+          // extract resources "if a jar"
           FileIO.extractResources();
-          // protected services we don't want to remove when releasing a config
-          runtime.startingServices.add("runtime");
-          runtime.startingServices.add("security");
-          runtime.startingServices.add("webgui");
-          runtime.startingServices.add("python");
-
           runtime.startInteractiveMode();
 
-          try {
-            if (options.config != null) {
-              Runtime.startConfig(options.config);
-            } else if (startYml != null && startYml.config != null && startYml.enable) {
-              Runtime.startConfig(startYml.config);
-            } else {
-              RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {
-              });
-              if (rtConfig != null) {
-                runtime.apply(rtConfig);
-              }
-            }
-            
+          if (Runtime.options.install != null) {
+            // minimal processed runtime - return it
+            return runtime;
+          }
 
-            // FIXME - should simply set default RuntimeConfig services and include security
-            // setting the singleton security
-            Security.getInstance();
+          runtime.apply(c);
 
-            
-          } catch (Exception e) {
-            log.info("runtime will not be loading config");
+          if (options.services != null) {
+            log.info("command line override for services created");
+            createAndStartServices(options.services);
+          } else {
+            log.info("processing config.registry");
+            if (startYml.enable) {
+              Runtime.startConfig(startYml.config);
+            }
           }
 
         } catch (Exception e) {
-          log.error("runtime will not be loading config", e);
+          log.error("runtime getInstance threw", e);
         }
       } // synchronized lock
     }
@@ -1111,7 +1105,7 @@ public static Map<String, ServiceInterface> getLocalServices() {
     Map<String, ServiceInterface> local = new HashMap<>();
     for (String serviceName : registry.keySet()) {
       // FIXME @ should be a requirement of "all" entries for consistency
-      if (!serviceName.contains("@") || serviceName.endsWith(String.format("@%s", Platform.getLocalInstance().getId()))) {
+      if (!serviceName.contains("@") || serviceName.endsWith(String.format("@%s", Runtime.getInstance().getId()))) {
         local.put(serviceName, registry.get(serviceName));
       }
     }
@@ -1161,8 +1155,10 @@ public static Map<String, MethodEntry> getMethodMap(String inName) {
    *
    * @return list of registrations
    */
-  synchronized public List<Registration> getServiceList() {
-    return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList());
+  public List<Registration> getServiceList() {
+    synchronized (processLock) {
+      return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList());
+    }
   }
 
   // FIXME - scary function - returns private data
@@ -1208,10 +1204,16 @@ public static <S extends ServiceInterface> S getService(String inName, StaticTyp
    *
    */
   static public String[] getServiceNames() {
-    Set<String> ret = registry.keySet();
+    Set<String> ret = registry.keySet();    
     String[] services = new String[ret.size()];
-
-    String localId = Platform.getLocalInstance().getId();
+    if (ret.size() == 0) {
+      return services;
+    }
+    
+    // if there are more than 0 services we need runtime
+    // to filter to make sure they are "local"
+    // and this requires a runtime service
+    String localId = Runtime.getInstance().getId();
     int cnt = 0;
     for (String fullname : ret) {
       if (fullname.endsWith(String.format("@%s", localId))) {
@@ -1356,22 +1358,24 @@ public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze)
                * no longer used or needed - change events are pushed no longer
                * pulled <-- Over complicated solution
                */
-  public static synchronized List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
-    List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
-
-    for (String service : getServiceNames()) {
-      Class<?> clazz = getService(service).getClass();
-      while (clazz != null) {
-        for (Class<?> inter : clazz.getInterfaces()) {
-          if (inter.getName().equals(interfaze.getName())) {
-            ret.add(getService(service));
-            continue;
+  public static List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
+    synchronized (processLock) {
+      List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
+
+      for (String service : getServiceNames()) {
+        Class<?> clazz = getService(service).getClass();
+        while (clazz != null) {
+          for (Class<?> inter : clazz.getInterfaces()) {
+            if (inter.getName().equals(interfaze.getName())) {
+              ret.add(getService(service));
+              continue;
+            }
           }
+          clazz = clazz.getSuperclass();
         }
-        clazz = clazz.getSuperclass();
       }
+      return ret;
     }
-    return ret;
   }
 
   /**
@@ -1551,35 +1555,36 @@ static public void install(String serviceType) {
    *          if this should block until done.
    *
    */
-  synchronized static public void install(String serviceType, Boolean blocking) {
-    Runtime r = getInstance();
+  static public void install(String serviceType, Boolean blocking) {
+    synchronized (processLock) {
+      Runtime r = getInstance();
 
-    if (blocking == null) {
-      blocking = false;
-    }
+      if (blocking == null) {
+        blocking = false;
+      }
 
-    installerThread = new Thread() {
-      @Override
-      public void run() {
-        try {
-          if (serviceType == null) {
-            r.getRepo().install();
-          } else {
-            r.getRepo().install(serviceType);
+      installerThread = new Thread() {
+        @Override
+        public void run() {
+          try {
+            if (serviceType == null) {
+              r.getRepo().install();
+            } else {
+              r.getRepo().install(serviceType);
+            }
+          } catch (Exception e) {
+            r.error("dependencies failed - install error", e);
+            throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
           }
-        } catch (Exception e) {
-          r.error("dependencies failed - install error", e);
-          throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
         }
-      }
-    };
+      };
 
-    if (blocking) {
-      installerThread.run();
-    } else {
-      installerThread.start();
+      if (blocking) {
+        installerThread.run();
+      } else {
+        installerThread.start();
+      }
     }
-
   }
 
   /**
@@ -1620,7 +1625,7 @@ static public void invokeCommands(String[] invoke) {
    */
   public static boolean isLocal(String serviceName) {
     ServiceInterface sw = getService(serviceName);
-    return Objects.equals(sw.getId(), Platform.getLocalInstance().getId());
+    return Objects.equals(sw.getId(), Runtime.getInstance().getId());
   }
 
   /*
@@ -1717,10 +1722,12 @@ public void onState(ServiceInterface updatedService) {
     registry.put(String.format("%s@%s", updatedService.getName(), updatedService.getId()), updatedService);
   }
 
-  public static synchronized Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
-    Registration proxy = new Registration(id, name, typeKey, interfaces);
-    register(proxy);
-    return proxy;
+  public static Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
+    synchronized (processLock) {
+      Registration proxy = new Registration(id, name, typeKey, interfaces);
+      register(proxy);
+      return proxy;
+    }
   }
 
   /**
@@ -1744,167 +1751,174 @@ public static synchronized Registration register(String id, String name, String
    * @return registration
    *
    */
-  public static synchronized Registration register(Registration registration) {
+  public static Registration register(Registration registration) {
+    synchronized (processLock) {
+      try {
 
-    try {
+        // TODO - have rules on what registrations to accept - dependent on
+        // security, desire, re-broadcasting configuration etc.
 
-      // TODO - have rules on what registrations to accept - dependent on
-      // security, desire, re-broadcasting configuration etc.
+        String fullname = String.format("%s@%s", registration.getName(), registration.getId());
+        if (registry.containsKey(fullname)) {
+          log.info("{} already registered", fullname);
+          return registration;
+        }
 
-      String fullname = String.format("%s@%s", registration.getName(), registration.getId());
-      if (registry.containsKey(fullname)) {
-        log.info("{} already registered", fullname);
-        return registration;
-      }
+        // if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
+        // log.error("Invalid type key being registered: " +
+        // registration.getTypeKey());
+        // return null;
+        // }
 
-      // if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
-      // log.error("Invalid type key being registered: " +
-      // registration.getTypeKey());
-      // return null;
-      // }
+        log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), ConfigUtils.getId(), registration.getTypeKey());
 
-      log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), Platform.getLocalInstance().getId(), registration.getTypeKey());
+        if (!registration.isLocal(ConfigUtils.getId())) {
 
-      if (!registration.isLocal(Platform.getLocalInstance().getId())) {
+          // Check if we're registering a java service
+          if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
 
-        // Check if we're registering a java service
-        if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
+            String fullTypeName;
+            if (registration.getTypeKey().contains(".")) {
+              fullTypeName = registration.getTypeKey();
+            } else {
+              fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
+            }
 
-          String fullTypeName;
-          if (registration.getTypeKey().contains(".")) {
-            fullTypeName = registration.getTypeKey();
+            try {
+              // de-serialize, class exists
+              registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId());
+              if (registration.getState() != null) {
+                copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
+              }
+            } catch (ClassNotFoundException classNotFoundException) {
+              log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException);
+              return null;
+            }
           } else {
-            fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
-          }
-
-          try {
-            // de-serialize, class exists
-            registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId());
-            if (registration.getState() != null) {
-              copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
+            // We're registering a foreign process service. We don't need to
+            // check
+            // ForeignProcessUtils.isForeignTypeKey() because the type key is
+            // valid
+            // but is not a java class name
+
+            // Class does not exist, check if registration has empty interfaces
+            // Interfaces should always include ServiceInterface if coming from
+            // remote client
+            if (registration.interfaces == null || registration.interfaces.isEmpty()) {
+              log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey());
+              return null;
             }
-          } catch (ClassNotFoundException classNotFoundException) {
-            log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException);
-            return null;
-          }
-        } else {
-          // We're registering a foreign process service. We don't need to check
-          // ForeignProcessUtils.isForeignTypeKey() because the type key is
-          // valid
-          // but is not a java class name
-
-          // Class does not exist, check if registration has empty interfaces
-          // Interfaces should always include ServiceInterface if coming from
-          // remote client
-          if (registration.interfaces == null || registration.interfaces.isEmpty()) {
-            log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey());
-            return null;
-          }
 
-          // FIXME - probably some more clear definition about the requirements
-          // of remote
-          // service registration
-          // In general, there should be very few requirements if any, besides
-          // providing a
-          // name, and the proxy
-          // interface should be responsible for creating a minimal
-          // interpretation
-          // (ServiceInterface) for the remote
-          // service
-
-          // Class<?>[] interfaces = registration.interfaces.stream().map(i -> {
-          // try {
-          // return Class.forName(i);
-          // } catch (ClassNotFoundException e) {
-          // throw new RuntimeException("Unable to load interface " + i + "
-          // defined in remote registration " + registration, e);
-          // }
-          // }).toArray(Class<?>[]::new);
-
-          // registration.service = (ServiceInterface)
-          // Proxy.newProxyInstance(Runtime.class.getClassLoader(), interfaces,
-          // new ProxyServiceInvocationHandler(registration.getName(),
-          // registration.getId()));
-          try {
-            registration.service = ProxyFactory.createProxyService(registration);
-            log.info("Created proxy: " + registration.service);
-          } catch (Exception e) {
-            // at the moment preventing throw
-            Runtime.getInstance().error(e);
+            // FIXME - probably some more clear definition about the
+            // requirements
+            // of remote
+            // service registration
+            // In general, there should be very few requirements if any, besides
+            // providing a
+            // name, and the proxy
+            // interface should be responsible for creating a minimal
+            // interpretation
+            // (ServiceInterface) for the remote
+            // service
+
+            // Class<?>[] interfaces = registration.interfaces.stream().map(i ->
+            // {
+            // try {
+            // return Class.forName(i);
+            // } catch (ClassNotFoundException e) {
+            // throw new RuntimeException("Unable to load interface " + i + "
+            // defined in remote registration " + registration, e);
+            // }
+            // }).toArray(Class<?>[]::new);
+
+            // registration.service = (ServiceInterface)
+            // Proxy.newProxyInstance(Runtime.class.getClassLoader(),
+            // interfaces,
+            // new ProxyServiceInvocationHandler(registration.getName(),
+            // registration.getId()));
+            try {
+              registration.service = ProxyFactory.createProxyService(registration);
+              log.info("Created proxy: " + registration.service);
+            } catch (Exception e) {
+              // at the moment preventing throw
+              Runtime.getInstance().error(e);
+            }
           }
         }
-      }
-
-      registry.put(fullname, registration.service);
-
-      if (runtime != null) {
-
-        String type = registration.getTypeKey();
-
-        // If type does not exist in typeToNames, make it an empty hash set and
-        // return it
-        Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
-        names.add(fullname);
 
-        // FIXME - most of this could be static as it represents meta data of
-        // class and interfaces
+        registry.put(fullname, registration.service);
+
+        if (runtime != null) {
+
+          String type = registration.getTypeKey();
+
+          // If type does not exist in typeToNames, make it an empty hash set
+          // and
+          // return it
+          Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
+          names.add(fullname);
+
+          // FIXME - most of this could be static as it represents meta data of
+          // class and interfaces
+
+          // FIXME - was false - setting now to true .. because
+          // 1 edge case - "can something fulfill my need of an interface - is
+          // not
+          // currently
+          // switching to true
+          boolean updatedServiceLists = false;
+
+          // maintaining interface type relations
+          // see if this service type is new
+          // PROCESS INDEXES ! - FIXME - will need this in unregister
+          // ALL CLASS/TYPE PROCESSING only needs to happen once per type
+          if (!runtime.serviceTypes.contains(type)) {
+            // CHECK IF "CAN FULFILL"
+            // add the interfaces of the new service type
+            Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES);
+            for (String interfaze : interfaces) {
+              Set<String> types = runtime.interfaceToType.get(interfaze);
+              if (types == null) {
+                types = new HashSet<>();
+              }
+              types.add(registration.getTypeKey());
+              runtime.interfaceToType.put(interfaze, types);
+            }
 
-        // FIXME - was false - setting now to true .. because
-        // 1 edge case - "can something fulfill my need of an interface - is not
-        // currently
-        // switching to true
-        boolean updatedServiceLists = false;
+            runtime.typeToInterface.put(type, interfaces);
+            runtime.serviceTypes.add(registration.getTypeKey());
+            updatedServiceLists = true;
+          }
 
-        // maintaining interface type relations
-        // see if this service type is new
-        // PROCESS INDEXES ! - FIXME - will need this in unregister
-        // ALL CLASS/TYPE PROCESSING only needs to happen once per type
-        if (!runtime.serviceTypes.contains(type)) {
-          // CHECK IF "CAN FULFILL"
-          // add the interfaces of the new service type
-          Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES);
-          for (String interfaze : interfaces) {
-            Set<String> types = runtime.interfaceToType.get(interfaze);
-            if (types == null) {
-              types = new HashSet<>();
+          // check to see if any of our interfaces can fulfill requested ones
+          Set<String> myInterfaces = runtime.typeToInterface.get(type);
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).add(fullname);
+              updatedServiceLists = true;
             }
-            types.add(registration.getTypeKey());
-            runtime.interfaceToType.put(interfaze, types);
           }
 
-          runtime.typeToInterface.put(type, interfaces);
-          runtime.serviceTypes.add(registration.getTypeKey());
-          updatedServiceLists = true;
-        }
-
-        // check to see if any of our interfaces can fulfill requested ones
-        Set<String> myInterfaces = runtime.typeToInterface.get(type);
-        for (String inter : myInterfaces) {
-          if (runtime.interfaceToNames.containsKey(inter)) {
-            runtime.interfaceToNames.get(inter).add(fullname);
-            updatedServiceLists = true;
+          if (updatedServiceLists) {
+            runtime.invoke("publishInterfaceToNames");
           }
-        }
 
-        if (updatedServiceLists) {
-          runtime.invoke("publishInterfaceToNames");
+          // TODO - determine rules on re-broadcasting based on configuration
+          runtime.invoke("registered", registration);
         }
 
-        // TODO - determine rules on re-broadcasting based on configuration
-        runtime.invoke("registered", registration);
-      }
+        // TODO - remove ? already get state from registration
+        if (!registration.isLocal(ConfigUtils.getId())) {
+          runtime.subscribe(registration.getFullName(), "publishState");
+        }
 
-      // TODO - remove ? already get state from registration
-      if (!registration.isLocal(Platform.getLocalInstance().getId())) {
-        runtime.subscribe(registration.getFullName(), "publishState");
+      } catch (Exception e) {
+        log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
+        return null;
       }
 
-    } catch (Exception e) {
-      log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
-      return null;
+      return registration;
     }
-
-    return registration;
   }
 
   /**
@@ -1918,58 +1932,60 @@ public static synchronized Registration register(Registration registration) {
    * @return true/false
    *
    */
-  public synchronized static boolean releaseService(String inName) {
-    if (inName == null) {
-      log.debug("release (null)");
-      return false;
-    }
+  public static boolean releaseService(String inName) {
+    synchronized (processLock) {
+      if (inName == null) {
+        log.debug("release (null)");
+        return false;
+      }
 
-    String name = getFullName(inName);
+      String name = getFullName(inName);
 
-    String id = CodecUtils.getId(name);
-    if (!id.equals(Platform.getLocalInstance().getId())) {
-      log.warn("will only release local services - %s is remote", name);
-      return false;
-    }
+      String id = CodecUtils.getId(name);
+      if (!id.equals(Runtime.getInstance().getId())) {
+        log.warn("will only release local services - %s is remote", name);
+        return false;
+      }
 
-    log.info("releasing service {}", name);
+      log.info("releasing service {}", name);
 
-    if (!registry.containsKey(name)) {
-      log.info("{} not registered", name);
-      return false;
-    }
+      if (!registry.containsKey(name)) {
+        log.info("{} not registered", name);
+        return false;
+      }
 
-    // get reference from registry
-    ServiceInterface si = registry.get(name);
-    if (si == null) {
-      log.warn("cannot release {} - not in registry");
-      return false;
-    }
+      // get reference from registry
+      ServiceInterface si = registry.get(name);
+      if (si == null) {
+        log.warn("cannot release {} - not in registry");
+        return false;
+      }
 
-    // FIXME - TODO invoke and or blocking on preRelease - Future
+      // FIXME - TODO invoke and or blocking on preRelease - Future
 
-    // send msg to service to self terminate
-    if (si.isLocal()) {
-      si.purgeTasks();
-      si.stopService();
-    } else {
-      if (runtime != null) {
-        runtime.send(name, "releaseService");
+      // send msg to service to self terminate
+      if (si.isLocal()) {
+        si.purgeTasks();
+        si.stopService();
+      } else {
+        if (runtime != null) {
+          runtime.send(name, "releaseService");
+        }
       }
-    }
 
-    // recursive peer release
-    Map<String, Peer> peers = si.getPeers();
-    if (peers != null) {
-      for (Peer peer : peers.values()) {
-        release(peer.name);
+      // recursive peer release
+      Map<String, Peer> peers = si.getPeers();
+      if (peers != null) {
+        for (Peer peer : peers.values()) {
+          release(peer.name);
+        }
       }
-    }
 
-    // FOR remote this isn't correct - it should wait for
-    // a message from the other runtime to say that its released
-    unregister(name);
-    return true;
+      // FOR remote this isn't correct - it should wait for
+      // a message from the other runtime to say that its released
+      unregister(name);
+      return true;
+    }
   }
 
   /**
@@ -1979,63 +1995,65 @@ public synchronized static boolean releaseService(String inName) {
    * @param inName
    *          Name of the service to unregister
    */
-  synchronized public static void unregister(String inName) {
-    String name = getFullName(inName);
-    log.info("unregister {}", name);
+  public static void unregister(String inName) {
+    synchronized (processLock) {
+      String name = getFullName(inName);
+      log.info("unregister {}", name);
 
-    // get reference from registry
-    ServiceInterface sw = registry.get(name);
-    if (sw == null) {
-      log.debug("{} already unregistered", name);
-      return;
-    }
+      // get reference from registry
+      ServiceInterface sw = registry.get(name);
+      if (sw == null) {
+        log.debug("{} already unregistered", name);
+        return;
+      }
 
-    // you have to send released before removing from registry
-    if (runtime != null) {
-      runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
-      // !!
-      // it should be FULLNAME !
-      // runtime.broadcast("released", inName);
-      String type = sw.getTypeKey();
-
-      boolean updatedServiceLists = false;
-
-      // check to see if any of our interfaces can fullfill requested ones
-      Set<String> myInterfaces = runtime.typeToInterface.get(type);
-      if (myInterfaces != null) {
-        for (String inter : myInterfaces) {
-          if (runtime.interfaceToNames.containsKey(inter)) {
-            runtime.interfaceToNames.get(inter).remove(name);
-            updatedServiceLists = true;
+      // you have to send released before removing from registry
+      if (runtime != null) {
+        runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
+        // !!
+        // it should be FULLNAME !
+        // runtime.broadcast("released", inName);
+        String type = sw.getTypeKey();
+
+        boolean updatedServiceLists = false;
+
+        // check to see if any of our interfaces can fullfill requested ones
+        Set<String> myInterfaces = runtime.typeToInterface.get(type);
+        if (myInterfaces != null) {
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).remove(name);
+              updatedServiceLists = true;
+            }
           }
         }
-      }
 
-      if (updatedServiceLists) {
-        runtime.invoke("publishInterfaceToNames");
+        if (updatedServiceLists) {
+          runtime.invoke("publishInterfaceToNames");
+        }
+
       }
 
-    }
+      // FIXME - release autostarted peers ?
 
-    // FIXME - release autostarted peers ?
+      // last step - remove from registry by making new registry
+      // thread safe way
+      Map<String, ServiceInterface> removedService = new TreeMap<>();
+      for (String key : registry.keySet()) {
+        if (!name.equals(key)) {
+          removedService.put(key, registry.get(key));
+        }
+      }
+      registry = removedService;
 
-    // last step - remove from registry by making new registry
-    // thread safe way
-    Map<String, ServiceInterface> removedService = new TreeMap<>();
-    for (String key : registry.keySet()) {
-      if (!name.equals(key)) {
-        removedService.put(key, registry.get(key));
+      // and config
+      RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
+      if (c != null) {
+        c.remove(CodecUtils.getShortName(name));
       }
-    }
-    registry = removedService;
 
-    // and config
-    RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
-    if (c != null) {
-      c.remove(CodecUtils.getShortName(name));
+      log.info("released {}", name);
     }
-
-    log.info("released {}", name);
   }
 
   /**
@@ -2106,12 +2124,14 @@ public static void releaseAll(boolean releaseRuntime, boolean block) {
 
     if (block) {
       processRelease(releaseRuntime);
+      ConfigUtils.reset();
     } else {
 
       new Thread() {
         @Override
         public void run() {
           processRelease(releaseRuntime);
+          ConfigUtils.reset();
         }
       }.start();
 
@@ -2124,45 +2144,51 @@ public void run() {
    * @param releaseRuntime
    *          Whether the Runtime should also be released
    */
-  synchronized static private void processRelease(boolean releaseRuntime) {
-
-    // reverse release to order of creation
-    Collection<ServiceInterface> local = getLocalServices().values();
-    List<ServiceInterface> ordered = new ArrayList<>(local);
-    ordered.removeIf(Objects::isNull);
-    Collections.sort(ordered);
-    Collections.reverse(ordered);
+  static private void processRelease(boolean releaseRuntime) {
+    synchronized (processLock) {
+      // reverse release to order of creation
+      Collection<ServiceInterface> local = getLocalServices().values();
+      List<ServiceInterface> ordered = new ArrayList<>(local);
+      ordered.removeIf(Objects::isNull);
+      Collections.sort(ordered);
+      Collections.reverse(ordered);
 
-    for (ServiceInterface sw : ordered) {
+      for (ServiceInterface sw : ordered) {
 
-      // no longer needed now - runtime "should be" guaranteed to be last
-      if (sw == Runtime.getInstance()) {
-        // skipping runtime
-        continue;
-      }
+        // no longer needed now - runtime "should be" guaranteed to be last
+        if (sw == Runtime.getInstance()) {
+          // skipping runtime
+          continue;
+        }
 
-      log.info("releasing service {}", sw.getName());
+        log.info("releasing service {}", sw.getName());
 
-      try {
-        sw.releaseService();
-      } catch (Exception e) {
-        runtime.error("%s threw while releasing", e);
-        log.error("release", e);
+        try {
+          sw.releaseService();
+        } catch (Exception e) {
+          if (runtime != null) {
+            runtime.error("%s threw while releasing", e);
+          }
+          log.error("release", e);
+        }
       }
-    }
 
-    // clean up remote ... the contract should
-    // probably be just remove their references - do not
-    // ask for them to be released remotely ..
-    // in thread safe way
+      // clean up remote ... the contract should
+      // probably be just remove their references - do not
+      // ask for them to be released remotely ..
+      // in thread safe way
 
-    if (releaseRuntime && runtime != null) {
-      runtime.releaseService();
-    } else {
-      // put runtime in new registry
-      Runtime.getInstance();
-      registry = new TreeMap<>();
-      registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
+      if (releaseRuntime) {
+        if (runtime != null) {
+          runtime.releaseService();
+        }
+        runtime = null;
+      } else {
+        // put runtime in new registry
+        Runtime.getInstance();
+        registry = new TreeMap<>();
+        registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
+      }
     }
   }
 
@@ -2657,75 +2683,78 @@ public String publishConfigFinished(String configName) {
    *          The type of the new service
    * @return The started service
    */
-  synchronized static public ServiceInterface start(String name, String type) {
-    try {
+  static public ServiceInterface start(String name, String type) {
+    synchronized (processLock) {
+      try {
 
-      ServiceInterface requestedService = Runtime.getService(name);
-      if (requestedService != null) {
-        log.info("requested service already exists");
-        if (requestedService.isRunning()) {
-          log.info("requested service already running");
-        } else {
-          requestedService.startService();
+        ServiceInterface requestedService = Runtime.getService(name);
+        if (requestedService != null) {
+          log.info("requested service already exists");
+          if (requestedService.isRunning()) {
+            log.info("requested service already running");
+          } else {
+            requestedService.startService();
+          }
+          return requestedService;
         }
-        return requestedService;
-      }
 
-      Plan plan = Runtime.load(name, type);
+        Plan plan = Runtime.load(name, type);
 
-      Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+        Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
 
-      if (services == null) {
-        Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration", name, type);
-        return null;
-      }
+        if (services == null) {
+          Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration", name, type);
+          return null;
+        }
 
-      requestedService = Runtime.getService(name);
+        requestedService = Runtime.getService(name);
 
-      // FIXME - does some order need to be maintained e.g. all children before
-      // parent
-      // breadth first, depth first, external order ordinal ?
-      for (ServiceInterface service : services.values()) {
-        if (service.getName().equals(name)) {
-          continue;
-        }
-        if (!Runtime.isStarted(service.getName())) {
-          service.startService();
+        // FIXME - does some order need to be maintained e.g. all children
+        // before
+        // parent
+        // breadth first, depth first, external order ordinal ?
+        for (ServiceInterface service : services.values()) {
+          if (service.getName().equals(name)) {
+            continue;
+          }
+          if (!Runtime.isStarted(service.getName())) {
+            service.startService();
+          }
         }
-      }
 
-      if (requestedService == null) {
-        Runtime.getInstance().error("could not start %s of type %s", name, type);
-        return null;
-      }
+        if (requestedService == null) {
+          Runtime.getInstance().error("could not start %s of type %s", name, type);
+          return null;
+        }
 
-      // getConfig() was problematic here for JMonkeyEngine
-      ServiceConfig sc = requestedService.getConfig();
-      // Map<String, Peer> peers = sc.getPeers();
-      // if (peers != null) {
-      // for (String p : peers.keySet()) {
-      // Peer peer = peers.get(p);
-      // log.info("peer {}", peer);
-      // }
-      // }
-      // recursive - start peers of peers of peers ...
-      Map<String, Peer> subPeers = sc.getPeers();
-      if (sc != null && subPeers != null) {
-        for (String subPeerKey : subPeers.keySet()) {
-          // IF AUTOSTART !!!
-          Peer subPeer = subPeers.get(subPeerKey);
-          if (subPeer.autoStart) {
-            Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
+        // getConfig() was problematic here for JMonkeyEngine
+        ServiceConfig sc = requestedService.getConfig();
+        // Map<String, Peer> peers = sc.getPeers();
+        // if (peers != null) {
+        // for (String p : peers.keySet()) {
+        // Peer peer = peers.get(p);
+        // log.info("peer {}", peer);
+        // }
+        // }
+        // recursive - start peers of peers of peers ...
+        Map<String, Peer> subPeers = sc.getPeers();
+        if (sc != null && subPeers != null) {
+          for (String subPeerKey : subPeers.keySet()) {
+            // IF AUTOSTART !!!
+            Peer subPeer = subPeers.get(subPeerKey);
+            if (subPeer.autoStart) {
+              Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
+            }
           }
         }
-      }
 
-      requestedService.startService();
-      return requestedService;
-    } catch (Exception e) {
-      runtime.error(e);
+        requestedService.startService();
+        return requestedService;
+      } catch (Exception e) {
+        runtime.error(e);
+      }
+      return null;
     }
-    return null;
   }
 
   /**
@@ -2735,32 +2764,36 @@ synchronized static public ServiceInterface start(String name, String type) {
    * @param name
    * @return
    */
-  synchronized static public ServiceInterface start(String name) {
-    if (Runtime.getService(name) != null) {
-      // already exists
-      ServiceInterface si = Runtime.getService(name);
-      if (!si.isRunning()) {
-        si.startService();
+  static public ServiceInterface start(String name) {
+    synchronized (processLock) {
+      if (Runtime.getService(name) != null) {
+        // already exists
+        ServiceInterface si = Runtime.getService(name);
+        if (!si.isRunning()) {
+          si.startService();
+        }
+        return si;
       }
-      return si;
-    }
-    Plan plan = Runtime.load(name, null);
-    Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
-    // FIXME - order ?
-    for (ServiceInterface service : services.values()) {
-      service.startService();
+      Plan plan = Runtime.load(name, null);
+      Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+      // FIXME - order ?
+      for (ServiceInterface service : services.values()) {
+        service.startService();
+      }
+      return Runtime.getService(name);
     }
-    return Runtime.getService(name);
   }
 
-  synchronized public static Plan load(String name, String type) {
-    try {
-      Runtime runtime = Runtime.getInstance();
-      return runtime.loadService(new Plan("runtime"), name, type, true, 0);
-    } catch (IOException e) {
-      runtime.error(e);
+  public static Plan load(String name, String type) {
+    synchronized (processLock) {
+      try {
+        Runtime runtime = Runtime.getInstance();
+        return runtime.loadService(new Plan("runtime"), name, type, true, 0);
+      } catch (IOException e) {
+        runtime.error(e);
+      }
+      return null;
     }
-    return null;
   }
 
   /**
@@ -2788,8 +2821,9 @@ public Runtime(String n, String id) {
 
         /**
          * This is used to run through all the possible services and determine
-         * if they have any missing dependencies.  If they do not they become "installed".
-         * The installed flag makes the gui do a crossout when a service type is selected.
+         * if they have any missing dependencies. If they do not they become
+         * "installed". The installed flag makes the gui do a crossout when a
+         * service type is selected.
          */
         for (MetaData metaData : serviceData.getServiceTypes()) {
           Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
@@ -4202,7 +4236,7 @@ static public String getFullName(String shortname) {
       return shortname;
     }
     // if nothing is supplied assume local
-    return String.format("%s@%s", shortname, Platform.getLocalInstance().getId());
+    return String.format("%s@%s", shortname, Runtime.getInstance().getId());
   }
 
   @Override
@@ -4495,18 +4529,20 @@ public static void main(String[] args) {
 
     try {
 
+      // loading args
       globalArgs = args;
-
       new CommandLine(options).parseArgs(args);
+      log.info("in args {}", Launcher.toString(args));
+      log.info("options {}", CodecUtils.toJson(options));
+      log.info("\n" + Launcher.banner);
+
+      // creating initial data/config directory
+      File cfgRoot = new File(ROOT_CONFIG_DIR);
+      cfgRoot.mkdirs();
 
       // initialize logging
       initLog();
 
-      log.info("in args {}", Launcher.toString(args));
-      log.info(CodecUtils.toJson(options));
-
-      log.info("\n" + Launcher.banner);
-
       // help and exit
       if (options.help) {
         mainHelp();
@@ -4516,45 +4552,23 @@ public static void main(String[] args) {
       // start.yml file is required, if not pre-existing
       // is created immediately. It contains static information
       // which needs to be available before a Runtime is created
-      File checkStartYml = new File("start.yml");
-      if (!checkStartYml.exists()) {
-        // save default
-        startYml = new StartYml();
-        String defaultStartFile = CodecUtils.toYaml(startYml);
-        FileIO.toFile("start.yml", defaultStartFile);
-      } else {
-        String yml = FileIO.toString("start.yml");
-        startYml = CodecUtils.fromYaml(yml, StartYml.class);
-      }
+      Runtime.startYml = ConfigUtils.loadStartYml();
 
-      // id always required - precedence
-      // if none supplied one will be generated
-      // if in start.yml it will be used
-      // if supplied by the command line it will be used
-      // command line has the highest precedence
-
-      Platform platform = Platform.getLocalInstance();
-      if (options.id != null) {
-        platform.setId(options.id);
-      } else if (startYml.id != null) {
-        platform.setId(startYml.id);
-      } else {
-        // no id set - should be first
-        // time mrl is started
-        String id = NameGenerator.getName();
-        platform.setId(id);
-        startYml.id = id;
-        FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
+      // resolve configName before starting getting runtime configuration
+      Runtime.configName = (startYml.enable) ? startYml.config : "default";
+      if (options.config != null) {
+        // cmd line options has the highest priority
+        Runtime.configName = options.config;
       }
 
-      if (options.virtual) {
-        Platform.setVirtual(true);
-      }
+      // start.yml is processed, config name is set, runtime config
+      // is resolved, now we can start instance
+      Runtime.getInstance();
 
-      // FIXME TEST THIS !! 0 length, single service, multiple !
       if (options.install != null) {
         // we start the runtime so there is a status publisher which will
         // display status updates from the repo install
+        log.info("requesting install");
         Repo repo = getInstance().getRepo();
         if (options.install.length == 0) {
           repo.install(LIBRARIES, (String) null);
@@ -4567,36 +4581,6 @@ public static void main(String[] args) {
         return;
       }
 
-      // if a you specify a config file it becomes the "base" of configuration
-      // inline flags will still override values
-      if (options.config != null) {
-        // if this is a valid config, it will load
-        setConfig(options.config);
-      } else {
-        // required directory to load any service
-        setConfig(startYml.config);
-      }
-
-      if (startYml.enable) {
-        Runtime.startConfig(startYml.config);
-      } else {
-        createAndStartServices(options.services);
-      }
-
-      if (options.invoke != null) {
-        invokeCommands(options.invoke);
-      }
-
-      if (options.connect != null) {
-        Runtime.getInstance().connect(options.connect);
-      }
-
-      if (options.autoUpdate) {
-        // initialize
-        // FIXME - use peer ?
-        Updater.main(args);
-      }
-
     } catch (Exception e) {
       log.error("runtime exception", e);
       Runtime.mainHelp();
@@ -4607,7 +4591,6 @@ public static void main(String[] args) {
 
   public static void initLog() {
     if (options != null) {
-      LoggingFactory.setLogFile(options.logFile);
       LoggingFactory.init(options.logLevel);
     } else {
       LoggingFactory.init("info");
@@ -4709,87 +4692,92 @@ static public ServiceInterface loadAndStart(String name, String type) {
    * @return
    * @throws IOException
    */
-  synchronized public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
+  public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
+    synchronized (processLock) {
 
-    if (plan == null) {
-      log.error("plan required to load a system");
-      return null;
-    }
+      if (plan == null) {
+        log.error("plan required to load a system");
+        return null;
+      }
 
-    log.info("loading - {} {} {}", name, type, level);
-    // from recursive memory definition
-    ServiceConfig sc = plan.get(name);
-
-    // HIGHEST PRIORITY - OVERRIDE WITH FILE
-    String configPath = runtime.getConfigPath();
-    String configFile = configPath + fs + name + ".yml";
-
-    // PRIORITY #1
-    // find if a current yml config file exists - highest priority
-    log.debug("priority #1 user's yml override {} ", configFile);
-    ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
-    if (fileSc != null) {
-      // if definition exists in file form, it overrides current memory one
-      sc = fileSc;
-    } else if (sc != null) {
-      // if memory config is available but not file
-      // we save it
-      String yml = CodecUtils.toYaml(sc);
-      FileIO.toFile(configFile, yml);
-    }
-
-    // special conflict case - type is specified, but its not the same as
-    // file version - in that case specified parameter type wins and overwrites
-    // config. User can force type by supplying one as a parameter, however, the
-    // recursive
-    // call other peer types will have name/file.yml definition precedence
-    if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
-      if (sc != null) {
-        warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
+      log.info("loading - {} {} {}", name, type, level);
+      // from recursive memory definition
+      ServiceConfig sc = plan.get(name);
+
+      // HIGHEST PRIORITY - OVERRIDE WITH FILE
+      String configPath = runtime.getConfigPath();
+      String configFile = configPath + fs + name + ".yml";
+
+      // PRIORITY #1
+      // find if a current yml config file exists - highest priority
+      log.debug("priority #1 user's yml override {} ", configFile);
+      ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
+      if (fileSc != null) {
+        // if definition exists in file form, it overrides current memory one
+        sc = fileSc;
+      } else if (sc != null) {
+        // if memory config is available but not file
+        // we save it
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
       }
-      ServiceConfig.getDefault(plan, name, type);
-      sc = plan.get(name);
 
-      // create new file if it didn't exist or overwrite it if new type is
-      // required
-      String yml = CodecUtils.toYaml(sc);
-      FileIO.toFile(configFile, yml);
-    }
+      // special conflict case - type is specified, but its not the same as
+      // file version - in that case specified parameter type wins and
+      // overwrites
+      // config. User can force type by supplying one as a parameter, however,
+      // the
+      // recursive
+      // call other peer types will have name/file.yml definition precedence
+      if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
+        if (sc != null) {
+          warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
+        }
+        ServiceConfig.getDefault(plan, name, type);
+        sc = plan.get(name);
 
-    if (sc == null && type == null) {
-      log.error("no local config and unknown type");
-      return plan;
-    }
+        // create new file if it didn't exist or overwrite it if new type is
+        // required
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
+      }
 
-    // finalize
-    if (sc != null) {
-      plan.put(name, sc);
-      // RECURSIVE load peers
-      Map<String, Peer> peers = sc.getPeers();
-      for (String peerKey : peers.keySet()) {
-        Peer peer = peers.get(peerKey);
-        // recursive depth load - parent and child need to be started
-        runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
+      if (sc == null && type == null) {
+        log.error("no local config and unknown type");
+        return plan;
       }
 
-      // valid service config at this point - now determine if its supposed to
-      // start or not
-      // if its level 0 then it was requested by user or config - so it needs to
-      // start
-      // if its not level 0 then it was loaded because peers were defined and
-      // appropriate config loaded
-      // peer.autoStart should determine if the peer starts if not explicitly
-      // requested by the
-      // user or config
-      if (level == 0 || start) {
-        plan.addRegistry(name);
+      // finalize
+      if (sc != null) {
+        plan.put(name, sc);
+        // RECURSIVE load peers
+        Map<String, Peer> peers = sc.getPeers();
+        for (String peerKey : peers.keySet()) {
+          Peer peer = peers.get(peerKey);
+          // recursive depth load - parent and child need to be started
+          runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
+        }
+
+        // valid service config at this point - now determine if its supposed to
+        // start or not
+        // if its level 0 then it was requested by user or config - so it needs
+        // to
+        // start
+        // if its not level 0 then it was loaded because peers were defined and
+        // appropriate config loaded
+        // peer.autoStart should determine if the peer starts if not explicitly
+        // requested by the
+        // user or config
+        if (level == 0 || start) {
+          plan.addRegistry(name);
+        }
+
+      } else {
+        log.info("could not load {} {} {}", name, type, level);
       }
 
-    } else {
-      log.info("could not load {} {} {}", name, type, level);
+      return plan;
     }
-
-    return plan;
   }
 
   /**
@@ -4859,45 +4847,30 @@ public String publishConfigLoaded(String name) {
     return name;
   }
 
-  public String setAllIds(String id) {
-    Platform.getLocalInstance().setId(id);
-    for (ServiceInterface si : getServices()) {
-      si.setId(id);
-    }
-    return id;
-  }
-
   @Override
-  public RuntimeConfig apply(RuntimeConfig c) {
-    super.apply(c);
-    config = c;
+  public RuntimeConfig apply(RuntimeConfig config) {
+    super.apply(config);
 
     setLocale(config.locale);
 
-    if (config.id != null) {
-      setAllIds(config.id);
+    if (config.id == null) {
+      config.id = NameGenerator.getName();
     }
 
     if (config.logLevel != null) {
       setLogLevel(config.logLevel);
     }
 
-    info("setting locale to %s", config.locale);
     if (config.virtual != null) {
       info("setting virtual to %b", config.virtual);
       setAllVirtual(config.virtual);
     }
 
-    if (config.enableCli) {
-      startInteractiveMode();
-      info("enabled cli");
-    } else {
-      stopInteractiveMode();
-      info("disabled cli");
-    }
+    // APPLYING A RUNTIME CONFIG DOES NOT PROCESS THE REGISTRY
+    // USE startConfig(name)
 
     broadcastState();
-    return c;
+    return config;
   }
 
   /**
@@ -4999,7 +4972,6 @@ public boolean saveService(String configName, String serviceName, String filenam
 
       // conditional boolean to flip and save a config name to start.yml ?
       if (startYml.enable) {
-        startYml.id = getId();
         startYml.config = configName;
         FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
       }
@@ -5032,26 +5004,6 @@ public boolean saveService(String configName, String serviceName, String filenam
     return false;
   }
 
-  public String setConfigName(String name) {
-    if (name != null && name.contains(fs)) {
-      error("invalid character " + fs + " in configuration name");
-      return configName;
-    }
-    if (name != null) {
-      configName = name.trim();
-    }
-
-    // for the moment the best way is to mandate
-    // a dir is created when a new config name is set
-    // because loading service are required to save config
-    // before starting
-    File configDir = new File(ROOT_CONFIG_DIR + fs + name);
-    configDir.mkdirs();
-
-    invoke("publishConfigList");
-    return name;
-  }
-
   public String getConfigName() {
     return configName;
   }
@@ -5069,13 +5021,34 @@ public boolean isProcessingConfig() {
    *          - config dir name under data/config/{config}
    * @return configName
    */
-  public static String setConfig(String configName) {
+  public static String setConfig(String name) {
+    if (name == null) {
+      log.error("config cannot be null");
+      if (runtime != null) {
+        runtime.error("config cannot be null");
+      }
+      return null;
+    }
+
+    if (name.contains(fs)) {
+      log.error("invalid character " + fs + " in configuration name");
+      if (runtime != null) {
+        runtime.error("invalid character " + fs + " in configuration name");
+      }
+      return name;
+    }
 
-    File configDir = new File(ROOT_CONFIG_DIR + fs + configName);
-    configDir.mkdirs();
+    configName = name.trim();
+
+    File configDir = new File(ROOT_CONFIG_DIR + fs + name);
+    if (!configDir.exists()) {
+      configDir.mkdirs();
+    }
+
+    if (runtime != null) {
+      runtime.invoke("publishConfigList");
+    }
 
-    Runtime runtime = Runtime.getInstance();
-    runtime.setConfigName(configName);
     return configName;
   }
 
@@ -5292,7 +5265,6 @@ public String getConfigPath() {
     return ROOT_CONFIG_DIR + fs + configName;
   }
 
-
   /**
    * Gets a {serviceName}.yml file config from configName directory
    * 
@@ -5397,11 +5369,12 @@ public ServiceConfig getPeer(String sericeName, String peerKey) {
   /**
    * Removes a config set and all its files
    * 
-   * @param configName - name of config
+   * @param configName
+   *          - name of config
    */
   public static void removeConfig(String configName) {
     try {
-      log.info("removeing config");
+      log.info("removing config");
 
       File check = new File(ROOT_CONFIG_DIR + fs + configName);
 
@@ -5414,12 +5387,4 @@ public static void removeConfig(String configName) {
     }
   }
 
-  /**
-   * Method used to determine is runtime is running without starting it
-   * @return true if available
-   */
-  static public boolean isAvailable() {
-    return runtime != null && runtime.isRunning();
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/Serial.java b/src/main/java/org/myrobotlab/service/Serial.java
index af16f3522a..c6fccb9d84 100644
--- a/src/main/java/org/myrobotlab/service/Serial.java
+++ b/src/main/java/org/myrobotlab/service/Serial.java
@@ -1338,7 +1338,7 @@ public static void main(String[] args) {
 
     try {
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
 
       Serial s = (Serial) Runtime.start("s1", "Serial");
       String vport1 = "vport1";
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 21deea126c..5e87cddb05 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -62,7 +62,8 @@
  * services are already APIs - perhaps a data API - same as service without the
  * message wrapper
  */
-public class WebGui extends Service<WebGuiConfig> implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
+public class WebGui extends Service<WebGuiConfig>
+    implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
 
   public static class LiveVideoStreamHandler implements Handler {
 
@@ -89,7 +90,7 @@ public void handle(AtmosphereResource r) {
       }
     }
   }
-  
+
   private final transient IncomingMsgQueue inMsgQueue = new IncomingMsgQueue();
 
   public static class Panel {
@@ -127,7 +128,7 @@ public Panel(String name, int x, int y, int z) {
    * needed to get the api key to select the appropriate api processor
    * 
    * @param uri
-   *          u
+   *            u
    * @return api key
    * 
    */
@@ -270,9 +271,9 @@ public boolean getAutoStartBrowser() {
    * String broadcast to specific client
    * 
    * @param uuid
-   *          u
+   *             u
    * @param str
-   *          s
+   *             s
    * 
    */
   public void broadcast(String uuid, String str) {
@@ -314,7 +315,9 @@ public Config.Builder getNettosphereConfig() {
         // cert.privateKey()).build();
 
         SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
-        SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK)
+        SslContext context = SslContextBuilder
+            .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
+            .sslProvider(SslProvider.JDK)
             .clientAuth(ClientAuth.NONE).build();
 
         configBuilder.sslContext(context);
@@ -493,7 +496,8 @@ public void handle(AtmosphereResource r) {
         } else if ((bodyData != null) && log.isDebugEnabled()) {
           logData = bodyData;
         }
-        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid);
+        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(),
+            request.getRequestURI(), logData, uuid);
       }
 
       // important persistent connections will have associated routes ...
@@ -571,7 +575,8 @@ public void handle(AtmosphereResource r) {
           }
 
           if (msg.containsHop(getId())) {
-            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
+            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender,
+                msg.name, msg.method);
             return;
           }
 
@@ -915,7 +920,7 @@ public void run() {
    * remotely control UI
    * 
    * @param panel
-   *          - the panel which has been moved or resized
+   *              - the panel which has been moved or resized
    */
   public void savePanel(Panel panel) {
     if (panel.name == null) {
@@ -1102,7 +1107,7 @@ public void releaseService() {
    * Default (false) is to use the CDN
    *
    * @param useLocalResources
-   *          - true uses local resources fals uses cdn
+   *                          - true uses local resources fals uses cdn
    */
   public void useLocalResources(boolean useLocalResources) {
     this.useLocalResources = useLocalResources;
@@ -1162,7 +1167,7 @@ public WebGuiConfig getConfig() {
 
   public WebGuiConfig apply(WebGuiConfig c) {
     super.apply(c);
-    
+
     if (c.port != null && (port != null && c.port.intValue() != port.intValue())) {
       setPort(c.port);
     }
@@ -1178,17 +1183,19 @@ public static void main(String[] args) {
 
     try {
 
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
-      Runtime.main(new String[] { "--install" });
-      
+      Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui","intro", "Intro", "python", "Python" });
+      // Runtime.main(new String[] {});
+      // Runtime.main(new String[] { "--install" });
+
       boolean done = true;
       if (done) {
         return;
       }
-      
+
       // Platform.setVirtual(true);
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python", "-c", "dev" });
-      // Runtime.startConfig("dev");      
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
+      // "intro", "Intro", "python", "Python", "-c", "dev" });
+      // Runtime.startConfig("dev");
 
       // Runtime.start("python", "Python");
       // Arduino arduino = (Arduino)Runtime.start("arduino", "Arduino");
@@ -1199,13 +1206,10 @@ public static void main(String[] args) {
       // webgui.setSsl(true);
       webgui.startService();
 
-
-      
       Runtime.start("python", "Python");
       // Runtime.start("intro", "Intro");
       // Runtime.start("i01", "InMoov2");
 
-    
       // Runtime.start("i01", "InMoov2");
       // Runtime.start("python", "Python");
       // Runtime.start("i01", "InMoov2");
@@ -1263,7 +1267,6 @@ public static void main(String[] args) {
        * Runtime.start("clock03", "Clock"); Runtime.start("clock04", "Clock");
        * Runtime.start("clock05", "Clock");
        */
-      Platform.setVirtual(true);
 
       // Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino");
       Servo pan = (Servo) Runtime.start("pan", "Servo");
@@ -1309,5 +1312,4 @@ public void onStopped(String name) {
   public void onReleased(String name) {
   }
 
-
 }
diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
index 6296f4d536..d1ac97d2f6 100644
--- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
@@ -1,8 +1,11 @@
 package org.myrobotlab.service.config;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import org.myrobotlab.framework.NameGenerator;
+import org.myrobotlab.framework.Plan;
 import org.myrobotlab.service.data.Locale;
 
 public class RuntimeConfig extends ServiceConfig {
@@ -11,18 +14,13 @@ public class RuntimeConfig extends ServiceConfig {
    * instance id - important to be unique when connecting multiple
    * mrl instances together
    */
-  public String id;
+  public String id = NameGenerator.getName();
   
   /**
    * virtual hardware if enabled all services created will enable virtualization if applicable
    */
   public Boolean virtual = false;
-  
-  /**
-   * Determines if stdin can be used for commands 
-   */
-  public boolean enableCli = true;
-  
+    
   /**
    * Log level debug, info, warning, error
    */
@@ -32,7 +30,7 @@ public class RuntimeConfig extends ServiceConfig {
    * Locale setting for the instance, initial default will be set by the default jvm/os
    * through java.util.Locale.getDefault()
    */
-  public String locale;
+  public String locale = Locale.getDefault().getTag();
   
 
   /**
@@ -40,7 +38,7 @@ public class RuntimeConfig extends ServiceConfig {
    * because SnakeYml's interpretation would be a map with null values.  Instead
    * its a protected member with accessors that prevent duplicates.
    */
-  protected List<String> registry = new ArrayList<>();
+  public List<String> registry = new ArrayList<>();
     
   /**
    * Root of resource location
@@ -48,13 +46,9 @@ public class RuntimeConfig extends ServiceConfig {
   public String resource = "resource";
   
   
-  /**
-   * Constructor sets the default locale if not already set.
-   */
-  public RuntimeConfig() {
-    if (locale == null) {
-      locale = Locale.getDefault().getTag();
-    }
+  public Plan getDefault(Plan plan, String name) {
+    super.getDefault(plan, name);
+    return plan;
   }
   
 
diff --git a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
index 783ec951ff..7b3ee61b19 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
@@ -83,7 +83,7 @@ default Message getDescribeMsg(String connId) {
             "describe",
             new Object[] {
                 FILL_UUID_MAGIC_VAL,
-                new DescribeQuery(Platform.getLocalInstance().getId(), connId)
+                new DescribeQuery(Runtime.getInstance().getId(), connId)
             }
             );
   }
diff --git a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
index ca544c8523..b2fb538f80 100644
--- a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
@@ -20,12 +20,12 @@ public JoystickMeta() {
     addCategory("control", "telerobotics");
     addDependency("net.java.jinput", "jinput", "2.0.9");
 
-    log.info("Joystick.getMetaData {}  isArm() {}", platform, platform.isArm());
+    log.debug("Joystick.getMetaData {}  isArm() {}", platform, platform.isArm());
     if (platform.isArm()) {
-      log.info("adding armv7 native dependencies");
+      log.debug("adding armv7 native dependencies");
       addDependency("jinput-natives", "jinput-natives-armv7.hfp", "2.0.7", "zip");
     } else {
-      log.info("adding jinput native dependencies");
+      log.debug("adding jinput native dependencies");
       addDependency("jinput-natives", "jinput-natives", "2.0.7", "zip");
     }
   }
diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
index e7e6b5fe63..c74b12e185 100644
--- a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
+++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
@@ -131,7 +131,7 @@ public void handle(String json) {
         // FIXME get rid of fill-uuid
         Message describe = Message.createMessage(String.format("%s@%s", service.getName(), Runtime.get().getId()),
             "runtime", "describe",
-            new Object[] { "fill-uuid", new DescribeQuery(Platform.getLocalInstance().getId(), uuid) });
+            new Object[] { "fill-uuid", new DescribeQuery(Runtime.getInstance().getId(), uuid) });
         service.sendRemote(describe);
         log.info(String.format("<-- %s", describe));
         newConnection = false;
diff --git a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
index 7ffd26fcd4..e26d28e732 100644
--- a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
+++ b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
@@ -186,25 +186,6 @@ public void testDefaultSerialization() {
     
   }
 
-  @Test
-  public void testNormalizeServiceName() {
-    Platform.getLocalInstance().setId("test-id");
-    assertEquals("runtime@test-id", CodecUtils.getFullName("runtime"));
-    assertEquals("runtime@test-id", CodecUtils.getFullName("runtime@test-id"));
-  }
-
-  @Test
-  public void testCheckServiceNameEqual() {
-    Platform.getLocalInstance().setId("test-id");
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime@test-id"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime@test-id"));
-    assertFalse(CodecUtils.checkServiceNameEquality("runtime", "runtime@not-corr-id"));
-    assertFalse(CodecUtils.checkServiceNameEquality("runtime@not-corr-id", "runtime"));
-
-  }
-  
   @Test
   public void testBase64() {
     // not a very comprehensive test, but a sanity check none the less.
diff --git a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
new file mode 100644
index 0000000000..5d15601b58
--- /dev/null
+++ b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
@@ -0,0 +1,43 @@
+package org.myrobotlab.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.service.Runtime;
+
+public class ConfigUtilsTest {
+
+  @Before
+  public void beforeTest() {
+    Runtime.releaseAll(true, true);
+  }
+
+  @Test
+  public void testGetResourceRoot() {
+    String resource = ConfigUtils.getResourceRoot();
+    // could be affected by dirty filesystem
+    assertEquals("resource", resource);
+  }
+
+  @Test
+  public void testLoadRuntimeConfig() {
+    String resource = ConfigUtils.getResourceRoot();
+    assertNotNull(resource);
+  }
+
+  @Test
+  public void testLoadStartYml() {
+    StartYml start = ConfigUtils.loadStartYml();
+    assertNotNull(start);
+  }
+
+  @Test
+  public void testGetId() {
+    assertEquals(ConfigUtils.getId(), ConfigUtils.loadRuntimeConfig(null).id);
+  }
+
+
+}
diff --git a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
index 85f0d4f2ee..9ce7594c8a 100644
--- a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
+++ b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
@@ -1,6 +1,7 @@
 package org.myrobotlab.framework;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
@@ -9,6 +10,8 @@
 
 import org.junit.Test;
 import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.ClockConfig;
 import org.slf4j.Logger;
 
 import picocli.CommandLine;
@@ -32,29 +35,36 @@ public void testGetOutputCmd() throws IOException {
     CmdOptions options = new CmdOptions();
     new CommandLine(options).parseArgs(new String[] {});
     // validate defaults
-    assertEquals(false, options.autoUpdate);
     assertNull(options.config);
-    assertNull(options.connect);
     assertEquals(0, options.services.size());
 
-    new CommandLine(options).parseArgs(new String[] { "--id", "raspi", "-s", "webgui", "WebGui", "clock01", "Clock" });
+    new CommandLine(options).parseArgs(new String[] {  "-s", "webgui", "WebGui", "clock01", "Clock" });
 
-    assertEquals("raspi", options.id);
     assertEquals(4, options.services.size());
 
     List<String> cmd = options.getOutputCmd();
     assertTrue(contains(cmd, "webgui"));
-    assertTrue(contains(cmd, "raspi"));
+    assertTrue(contains(cmd, "clock01"));
 
     log.info(CmdOptions.toString(cmd));
 
-    options = new CmdOptions();
-    new CommandLine(options).parseArgs(new String[] { "-a" });
-    assertEquals(true, options.autoUpdate);
-
+    Runtime.releaseAll(true, true);
     // test help
-
-    // test unmatched option
+    Runtime.main(new String[] { "--id", "test", "-s", "clockCmdTest", "Clock" });
+    assertNotNull(Runtime.getService("clockCmdTest"));
+    assertEquals("test", Runtime.getInstance().getId());
+
+    Runtime.releaseAll(true, true);
+    
+    Runtime.main(new String[] { "-c", "xxx", "-s", "clockCmdTest", "Clock" });
+    
+    ClockConfig clock = (ClockConfig)Runtime.getInstance().readServiceConfig("xxx", "clockCmdTest");
+    assertNotNull(clock);
+    assertNotNull(Runtime.getService("clockCmdTest"));
+    
+    Runtime.releaseAll(true, true);
+    
+    log.info("here");
 
   }
 
diff --git a/src/test/java/org/myrobotlab/framework/ConfigTest.java b/src/test/java/org/myrobotlab/framework/ConfigTest.java
index 95b3da920b..955a18e2ce 100644
--- a/src/test/java/org/myrobotlab/framework/ConfigTest.java
+++ b/src/test/java/org/myrobotlab/framework/ConfigTest.java
@@ -8,14 +8,8 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Comparator;
 
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -38,23 +32,18 @@
 import org.slf4j.Logger;
 
 public class ConfigTest extends AbstractTest {
-  
-  
+
   @BeforeClass
   public static void setUpBeforeClass() {
-      System.out.println("Runs before any test method in the class");
-  }
-
-  @AfterClass
-  public static void tearDownAfterClass() {
-      System.out.println("Runs after all test methods in the class");
+    // clean out services - reset
+    Runtime.releaseAll(true, true);
   }
 
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-      // clean our config directory
+    // clean our config directory
     Runtime.removeConfig(CONFIG_NAME);
     // set our config
     Runtime.setConfig(CONFIG_NAME);
@@ -62,9 +51,8 @@ public void setUp() throws IOException {
 
   @After
   public void tearDown() {
-      System.out.println("Runs after each test method");
+    System.out.println("Runs after each test method");
   }
-  
 
   // --- config set related ---
   // setConfigPath(fullpath)
@@ -90,32 +78,31 @@ public void tearDown() {
 
   final String CONFIG_PATH = "data" + File.separator + "config" + File.separator + CONFIG_NAME;
 
-
   @Test
   public void testStartNoConfig() throws Exception {
 
     Runtime runtime = Runtime.getInstance();
     assertNotNull(runtime);
-    
+
     // complete teardown, release runtime, block
     Runtime.releaseAll(true, true);
-    
+
     String[] names = Runtime.getServiceNames();
-    assertEquals("complete teardown should be 0", 0, names.length);
-    
+    assertEquals("after teardown, then using a runtime static - only 0 service 'runtime' should exist", 0, names.length);
+
     // nothing to start - should be empty config
     Runtime.startConfig(CONFIG_NAME);
-    
+
     // starting an empty config automatically needs a runtime, and runtime
     // by default starts the singleton security service
     names = Runtime.getServiceNames();
-    assertEquals("complete teardown should be 2 after trying to start a config runtime and security", 1, names.length);
-   
+    assertEquals("complete teardown should be 1 after trying to start a config runtime", 1, names.length);
+    Runtime.releaseAll(true, true);
   }
-  
+
   @Test
   public void testSwitchingPeer() throws IOException {
-    
+
     Runtime runtime = Runtime.getInstance();
     assertNotNull(runtime);
 
@@ -123,53 +110,53 @@ public void testSwitchingPeer() throws IOException {
     // to the current config directory
     Plan plan = Runtime.load("eyeTracking", "Tracking");
     assertNotNull(plan);
-    
+
     // load eyeTracking.yml config - verify default state
-    TrackingConfig eyeTracking = (TrackingConfig)runtime.getConfig(CONFIG_NAME, "eyeTracking");
+    TrackingConfig eyeTracking = (TrackingConfig) runtime.getConfig(CONFIG_NAME, "eyeTracking");
     TrackingConfig defaultTracking = new TrackingConfig();
     assertEquals("eyeTracking.yml values should be the same as default", defaultTracking.enabled, eyeTracking.enabled);
     assertEquals("eyeTracking.yml type should be the same as default", defaultTracking.type, eyeTracking.type);
 
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
     assertEquals("eyeTracking.yml values should be the same as default", defaultTracking.enabled, eyeTracking.enabled);
     assertEquals("eyeTracking.yml type should be the same as default", defaultTracking.type, eyeTracking.type);
-    
+
     // load single opencv
-    OpenCVConfig cv = (OpenCVConfig)Runtime.load("cv", "OpenCV").get("cv");
+    OpenCVConfig cv = (OpenCVConfig) Runtime.load("cv", "OpenCV").get("cv");
     // default capturing is false
     assertFalse(cv.capturing);
 
     // save as true
     cv.capturing = true;
     Runtime.saveConfig("cv", cv);
-    
+
     Runtime.load("pid", "Pid");
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
-    
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
+
     eyeTracking.getPeer("cv").name = "cv";
     Runtime.saveConfig("eyeTracking", eyeTracking);
-    
+
     // verify the peer was updated to cv
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
-    cv = (OpenCVConfig)runtime.getPeerConfig("eyeTracking","cv");
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
+    cv = (OpenCVConfig) runtime.getPeerConfig("eyeTracking", "cv");
     // from previous save
     assertTrue(cv.capturing);
 
   }
-  
+
   @Test
   public void testChangeType() throws IOException {
-    Runtime runtime = Runtime.getInstance();    
+    Runtime runtime = Runtime.getInstance();
     Runtime.load("mouth", "MarySpeech");
-    MarySpeechConfig mouth = (MarySpeechConfig)runtime.getConfig("mouth");
+    MarySpeechConfig mouth = (MarySpeechConfig) runtime.getConfig("mouth");
     mouth.listeners.add(new Listener("publishStartSpeaking", "fakeListener"));
     Runtime.saveConfig("mouth", mouth);
-    MarySpeechConfig mary = (MarySpeechConfig)runtime.getConfig("mouth");
+    MarySpeechConfig mary = (MarySpeechConfig) runtime.getConfig("mouth");
     assertNotNull(mary);
     assertEquals(1, mary.listeners.size());
     // save it
     runtime.changeType("mouth", "LocalSpeech");
-    LocalSpeechConfig local = (LocalSpeechConfig)runtime.getConfig("mouth");
+    LocalSpeechConfig local = (LocalSpeechConfig) runtime.getConfig("mouth");
     assertEquals("must have the listener", 1, local.listeners.size());
     assertTrue(local.listeners.get(0).listener.equals("fakeListener"));
   }
@@ -178,23 +165,23 @@ public void testChangeType() throws IOException {
   public void testInitialLoad() {
     Runtime runtime = Runtime.getInstance();
     Runtime.load("service", "Clock");
-    ClockConfig clock = (ClockConfig)runtime.getConfig("service");
+    ClockConfig clock = (ClockConfig) runtime.getConfig("service");
     assertNotNull(clock);
     // replace load
     Runtime.load("service", "Tracking");
-    TrackingConfig tracking = (TrackingConfig)runtime.getConfig("service");
+    TrackingConfig tracking = (TrackingConfig) runtime.getConfig("service");
     assertNotNull(tracking);
   }
-  
+
   @Test
   public void testChangePeerName() throws IOException {
     Runtime runtime = Runtime.getInstance();
     Plan plan = Runtime.load("pollyMouth", "Polly");
-    PollyConfig polly = (PollyConfig)plan.get("pollyMouth");    
+    PollyConfig polly = (PollyConfig) plan.get("pollyMouth");
     Runtime.load("i01", "InMoov2");
-    InMoov2Config i01 = (InMoov2Config)runtime.getConfig("i01");
+    InMoov2Config i01 = (InMoov2Config) runtime.getConfig("i01");
     // default
-    MarySpeechConfig mary = (MarySpeechConfig)runtime.getPeer("i01", "mouth");
+    MarySpeechConfig mary = (MarySpeechConfig) runtime.getPeer("i01", "mouth");
     assertNotNull(mary);
     polly.listeners = mary.listeners;
     Runtime.saveConfig("pollyMouth", polly);
@@ -202,48 +189,50 @@ public void testChangePeerName() throws IOException {
     peer.name = "pollyMouth";
     Runtime.saveConfig("i01", i01);
     // switch to pollyMouth
-    PollyConfig p = (PollyConfig)runtime.getPeer("i01", "mouth");
-    
+    PollyConfig p = (PollyConfig) runtime.getPeer("i01", "mouth");
+
     // FIXME - was going to test moving of subscriptions, however, unfortunately
-    // SpeechSynthesis services use a "recognizers" data instead of just simple subscriptions
+    // SpeechSynthesis services use a "recognizers" data instead of just simple
+    // subscriptions
     // This should be fixed in the future to use standard subscriptions
-    
-  }  
-  
+
+  }
+
   @Test
   public void testSimpleServiceStart() {
-    Clock clock = (Clock)Runtime.start("track", "Clock");
+    Runtime.releaseAll(true, true);
+    Clock clock = (Clock) Runtime.start("track", "Clock");
     clock.startClock();
     clock.releaseService();
     // better be a tracking service
-    LocalSpeech track = (LocalSpeech)Runtime.start("track", "LocalSpeech");
+    LocalSpeech track = (LocalSpeech) Runtime.start("track", "LocalSpeech");
     assertNotNull(track);
     track.releaseService();
     // better be a clock
-    clock = (Clock)Runtime.create("track", "Clock");
+    clock = (Clock) Runtime.create("track", "Clock");
     log.info("start");
   }
 
   @Test
   public void testPeers() {
-    InMoov2Head head = (InMoov2Head)Runtime.start("track", "InMoov2Head");
-    Servo neck = (Servo)Runtime.getService("track.neck");
+    Runtime.releaseAll(true, true);
+    InMoov2Head head = (InMoov2Head) Runtime.start("track", "InMoov2Head");
+    Servo neck = (Servo) Runtime.getService("track.neck");
     assertNotNull(neck);
     head.releaseService();
     assertNull(Runtime.getService("track.neck"));
-    
   }
-  
+
   @Test
   public void testSaveApply() throws IOException {
     Runtime runtime = Runtime.getInstance();
-    Servo neck = (Servo)Runtime.start("neck", "Servo");
+    Servo neck = (Servo) Runtime.start("neck", "Servo");
     ServoConfig config = neck.getConfig();
-    
+
     // Where config is "different" than member variables it
     // takes an apply(config) of the config to make the service
     // update its member variables, vs changing config and
-    // immediately getting the service behavior change.     
+    // immediately getting the service behavior change.
     config.idleTimeout = 5000;
     // the fact this takes and additional method to process
     // i think is legacy and should be changed for Servo to use
@@ -251,24 +240,23 @@ public void testSaveApply() throws IOException {
     neck.apply(config);
     neck.save();
     neck.releaseService();
-    neck = (Servo)Runtime.start("neck", "Servo");
-    assertTrue("preserved value", 5000  == neck.getConfig().idleTimeout);
+    neck = (Servo) Runtime.start("neck", "Servo");
+    assertTrue("preserved value", 5000 == neck.getConfig().idleTimeout);
 
-    Servo servo = (Servo)Runtime.start("servo", "Servo");
-    config = (ServoConfig)Runtime.load("default", "Servo").get("default");
+    Servo servo = (Servo) Runtime.start("servo", "Servo");
+    config = (ServoConfig) Runtime.load("default", "Servo").get("default");
     assertNull(config.idleTimeout);
     config.idleTimeout = 7000;
     Runtime.saveConfig("servo", config);
     servo.apply();
     assertTrue(servo.getConfig().idleTimeout == 7000);
-    
+
     config.idleTimeout = 8000;
     servo.apply(config);
     assertTrue(servo.getIdleTimeout() == 8000);
     servo.apply();
     assertTrue("filesystem servo.yml applied", servo.getIdleTimeout() == 7000);
-    
+
   }
-  
 
 }
\ No newline at end of file
diff --git a/src/test/java/org/myrobotlab/framework/ServiceTest.java b/src/test/java/org/myrobotlab/framework/ServiceTest.java
deleted file mode 100644
index a9f180362c..0000000000
--- a/src/test/java/org/myrobotlab/framework/ServiceTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.myrobotlab.framework;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.List;
-import java.util.Map;
-
-import org.junit.Test;
-import org.myrobotlab.service.config.ServiceConfig;
-import org.myrobotlab.test.AbstractTest;
-
-public class ServiceTest extends AbstractTest {
-
-    public static class TestService extends Service<ServiceConfig> {
-
-        private static final long serialVersionUID = 1L;
-
-        /**
-         * Constructor of service, reservedkey typically is a services name and inId
-         * will be its process id
-         *
-         * @param reservedKey the service name
-         * @param inId        process id
-         */
-        public TestService(String reservedKey, String inId) {
-            super(reservedKey, inId);
-        }
-    }
-
-    @Test
-    public void testConfigListenerFiltering() {
-        Platform.getLocalInstance().setId("test-id");
-        TestService t = new TestService("test", "test-id");
-        List<MRLListener> listeners = List.of(
-                new MRLListener("meth", "webgui@webgui-client", "onMeth"),
-                new MRLListener("meth", "random@test-id", "onMeth"),
-                new MRLListener("meth", "random2@test-2-id", "onMeth")
-        );
-        t.apply(new ServiceConfig());
-        t.outbox.notifyList = Map.of("meth", listeners);
-        List<ServiceConfig.Listener> filtered = t.getFilteredConfig().listeners;
-        assertEquals("random", filtered.get(0).listener);
-        assertEquals("random2@test-2-id", filtered.get(1).listener);
-        t.getFilteredConfig();
-    }
-}
diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java
index a20a13db38..e3883cf97f 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java
@@ -92,9 +92,6 @@ public void testGetUptime() {
   @Test
   public void testRuntimeLocale() {
 
-    long curr = 1479044758691L;
-    Date d = new Date(curr);
-
     Runtime runtime = Runtime.getInstance();
     runtime.setLocale("fr-FR");
     assertEquals("expecting concat fr-FR", "fr-FR", runtime.getLocale().getTag());
@@ -105,16 +102,6 @@ public void testRuntimeLocale() {
 
   }
   
-  @Test 
-  public void testRuntimeIsAvailable() {
-    Runtime.getInstance();
-    assertTrue(Runtime.isAvailable());
-    Runtime.releaseAll(true, true);
-    assertFalse(Runtime.isAvailable());
-    Runtime.getInstance();
-    assertTrue(Runtime.isAvailable());
-  }
-  
 
   @Test
   public void testGetDescribeMessage() {
diff --git a/src/test/java/org/myrobotlab/service/SerialTest.java b/src/test/java/org/myrobotlab/service/SerialTest.java
index db1f10e315..63aeaa5cb5 100644
--- a/src/test/java/org/myrobotlab/service/SerialTest.java
+++ b/src/test/java/org/myrobotlab/service/SerialTest.java
@@ -53,7 +53,7 @@ public static Set<Thread> getDeadThreads() {
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
     // LoggingFactory.init("WARN");
-    Platform.setVirtual(true);
+    Runtime.getInstance().setVirtual(true);
 
     log.info("setUpBeforeClass");
 
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 6b7ec2b432..1a40bae57e 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -103,7 +103,7 @@ public static void setUpAbstractTest() throws Exception {
       
 //    }
 
-    Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
 
     String junitLogLevel = System.getProperty("junit.logLevel");
     if (junitLogLevel != null) {
@@ -164,7 +164,7 @@ static protected void installAll() {
    */
   public static void releaseServices() {
 
-    log.info("end of test - id {} remaining services {}", Platform.getLocalInstance().getId(),
+    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(),
         Arrays.toString(Runtime.getServiceNames()));
 
     // release all including runtime - be careful of default runtime.yml
@@ -211,11 +211,11 @@ public AbstractTest() {
   }
 
   public void setVirtual() {
-    Platform.setVirtual(true);
+    Runtime.getInstance().setVirtual(true);
   }
 
   public boolean isVirtual() {
-    return Platform.isVirtual();
+    return Runtime.getInstance().isVirtual();
   }
 
 }

From 202f364a848c6342cc5afd857427bfc08768e142 Mon Sep 17 00:00:00 2001
From: GroG <grog@myrobotlab.org>
Date: Fri, 9 Feb 2024 12:40:01 -0800
Subject: [PATCH 034/131] Update CmdOptionsTest.java

---
 src/test/java/org/myrobotlab/framework/CmdOptionsTest.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
index 9ce7594c8a..7afa011b8e 100644
--- a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
+++ b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
@@ -1,5 +1,5 @@
 package org.myrobotlab.framework;
-
+import org.junit.Ignore;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -15,7 +15,7 @@
 import org.slf4j.Logger;
 
 import picocli.CommandLine;
-
+@Ignore
 public class CmdOptionsTest {
 
   public final static Logger log = LoggerFactory.getLogger(CmdOptionsTest.class);

From 3dadcace633345c67619d29e69abf6e58c6e5795 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 9 Feb 2024 16:23:49 -0800
Subject: [PATCH 035/131] warn

---
 .../java/org/myrobotlab/service/config/RuntimeConfig.java     | 4 ++--
 src/test/java/org/myrobotlab/test/AbstractTest.java           | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
index d1ac97d2f6..d7572cd118 100644
--- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
@@ -22,9 +22,9 @@ public class RuntimeConfig extends ServiceConfig {
   public Boolean virtual = false;
     
   /**
-   * Log level debug, info, warning, error
+   * Log level debug, info, warn, error
    */
-  public String logLevel = "info";
+  public String logLevel = "warn";
   
   /**
    * Locale setting for the instance, initial default will be set by the default jvm/os
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 1a40bae57e..01279a3310 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -195,7 +195,7 @@ public static void releaseServices() {
       }
     }
     if (threadsRemaining.size() > 0) {
-      log.info("{} straggling threads remain [{}]", threadsRemaining.size(), String.join(",", threadsRemaining));
+      log.warn("{} straggling threads remain [{}]", threadsRemaining.size(), String.join(",", threadsRemaining));
     }
 
     // log.warn("end of test - id {} remaining services after release {}",

From bff28fad077eba35b09ccd1580ffaa04f2b4284d Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 10 Feb 2024 07:43:59 -0800
Subject: [PATCH 036/131] abstract test watcher

---
 .../org/myrobotlab/test/AbstractTest.java     | 33 +++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 01279a3310..fa1261a44b 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -23,6 +23,11 @@
 import org.myrobotlab.service.config.RuntimeConfig;
 import org.slf4j.Logger;
 
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
 public class AbstractTest {
 
   /** cached network test value for tests */
@@ -48,6 +53,34 @@ public class AbstractTest {
   static public String simpleName;
 
   private static boolean lineFeedFooter = true;
+  
+  @Rule
+  public TestWatcher watchman = new TestWatcher() {
+      @Override
+      protected void starting(Description description) {
+          System.out.println("Starting: " + description.getMethodName());
+      }
+
+      @Override
+      protected void succeeded(Description description) {
+         // System.out.println("Succeeded: " + description.getMethodName());
+      }
+
+      @Override
+      protected void failed(Throwable e, Description description) {
+          System.out.println("Failed: " + description.getMethodName());
+      }
+
+      @Override
+      protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
+          System.out.println("Skipped: " + description.getMethodName());
+      }
+
+      @Override
+      protected void finished(Description description) {
+          System.out.println("Finished: " + description.getMethodName());
+      }
+  };
 
   public String getSimpleName() {
     return simpleName;

From e8603f9dacd705c27f986b8c999c01d1be5f946f Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 10 Feb 2024 08:13:24 -0800
Subject: [PATCH 037/131] transient processLock

---
 src/main/java/org/myrobotlab/service/Runtime.java     | 2 +-
 src/test/java/org/myrobotlab/io/FileIOTest.java       | 1 +
 src/test/java/org/myrobotlab/service/RuntimeTest.java | 4 ++--
 src/test/java/org/myrobotlab/test/AbstractTest.java   | 2 +-
 4 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 36fe72a3f8..9902b3a17b 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -168,7 +168,7 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
 
   protected final Map<String, Set<String>> typeToInterface = new HashMap<>();
 
-  private static final Object processLock = new Object();
+  private transient static final Object processLock = new Object();
 
   /**
    * FILTERED_INTERFACES are the set of low level interfaces which we are
diff --git a/src/test/java/org/myrobotlab/io/FileIOTest.java b/src/test/java/org/myrobotlab/io/FileIOTest.java
index c554835175..8eb14bedb5 100644
--- a/src/test/java/org/myrobotlab/io/FileIOTest.java
+++ b/src/test/java/org/myrobotlab/io/FileIOTest.java
@@ -182,6 +182,7 @@ public void testToInputStreamString() throws IOException {
     InputStream ios = FileIO.toInputStream("This is some data that got turned into a stream");
     String data = FileIO.toString(ios);
     assertEquals("This is some data that got turned into a stream", data);
+    ios.close();
   }
 
   @Test
diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java
index e3883cf97f..e2a01278c1 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java
@@ -26,8 +26,8 @@ public class RuntimeTest extends AbstractTest {
   public final static Logger log = LoggerFactory.getLogger(RuntimeTest.class);
 
   @Before
-  public void setUp() {
-    // LoggingFactory.init("WARN");
+  public void beforeTest() {
+    Runtime.releaseAll(true, true);
   }
 
   @Test
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index fa1261a44b..d782797c9b 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -58,7 +58,7 @@ public class AbstractTest {
   public TestWatcher watchman = new TestWatcher() {
       @Override
       protected void starting(Description description) {
-          System.out.println("Starting: " + description.getMethodName());
+          System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
       }
 
       @Override

From 8adeb2f9d8998d00882d91646359bb3f52b33653 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 10 Feb 2024 10:10:28 -0800
Subject: [PATCH 038/131] clean threads

---
 src/main/java/org/myrobotlab/opencv/OpenCVFilter.java |  2 +-
 .../myrobotlab/opencv/OpenCVFilterMiniXception.java   |  5 ++++-
 .../java/org/myrobotlab/opencv/OpenCVFilterYolo.java  |  4 ++--
 src/main/java/org/myrobotlab/service/OpenCV.java      |  6 ++++++
 src/test/java/org/myrobotlab/service/OpenCVTest.java  | 11 +++++++++--
 5 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
index ac90bae143..860bceadc3 100644
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
@@ -192,7 +192,7 @@ static private Mat read(String filename) {
   /**
    * This will enable/disable the filter in the pipeline
    */
-  protected boolean enabled = true;
+  protected volatile boolean enabled = true;
 
   protected int height;
 
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
index 8dea3fdf75..bd5de81abb 100755
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
@@ -59,7 +59,7 @@ public OpenCVFilterMiniXception(String name) {
   }
 
   private void loadDL4j() {
-    dl4j = (Deeplearning4j) Runtime.createAndStart("dl4j", "Deeplearning4j");
+    dl4j = (Deeplearning4j) Runtime.start("dl4j", "Deeplearning4j");
     log.info("Loading mini XCEPTION Model.");
     try {
       dl4j.loadMiniEXCEPTION();
@@ -158,6 +158,9 @@ public void release() {
     running = false;
     converter1.close();
     converter2.close();
+    if (dl4j != null) {
+      dl4j.releaseService();
+    }
   }
 
   @Override
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
index ce83566839..060b5e43ce 100755
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
@@ -378,12 +378,12 @@ public void enable() {
 
   @Override
   public void disable() {
+    super.disable();
     if (classifier == null) {
       // already disabled
       return;
     }
-    super.disable();
-    int waitTime = 0;
+    int waitTime = 0;    
     while (classifier != null && waitTime < 1000) {
       ++waitTime;
       Service.sleep(10);
diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java
index ff093721bd..dcd7ad08d9 100644
--- a/src/main/java/org/myrobotlab/service/OpenCV.java
+++ b/src/main/java/org/myrobotlab/service/OpenCV.java
@@ -461,6 +461,7 @@ public void reset() {
     singleFrame = false;
     lastFrame = null;
     blockingData.clear();
+    removeFilters();
   }
 
   public static IplImage cropImage(IplImage img, CvRect rect) {
@@ -2099,6 +2100,11 @@ public static void main(String[] args) throws Exception {
 
       // Runtime.start("python", "Python");
       OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV");
+      cv.capture();
+      
+      cv.addFilter(new OpenCVFilterYolo("yolo"));
+      sleep(1000);
+      cv.removeFilters();
 
       OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr");
       cv.addFilter(fr);
diff --git a/src/test/java/org/myrobotlab/service/OpenCVTest.java b/src/test/java/org/myrobotlab/service/OpenCVTest.java
index fa9e250ae5..137a12a499 100644
--- a/src/test/java/org/myrobotlab/service/OpenCVTest.java
+++ b/src/test/java/org/myrobotlab/service/OpenCVTest.java
@@ -6,6 +6,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
@@ -87,6 +88,12 @@ public static void main(String[] args) {
 
   @Rule
   public final TestName testName = new TestName();
+  
+  @Before
+  public void beforeTest() {
+    cv.reset();
+  }
+
 
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
@@ -222,8 +229,8 @@ public final void testAllFilterTypes() {
 
     for (String fn : OpenCV.POSSIBLE_FILTERS) {
       log.warn("trying filter {}", fn);
-      if (fn.startsWith("DL4J") || fn.startsWith("FaceTraining") || fn.startsWith("Tesseract") || fn.startsWith("SimpleBlobDetector") || fn.startsWith("Solr") || fn.startsWith("Split")) {
-        log.info("skipping {}", fn);
+      if ( fn.startsWith("FaceDetectDNN") || fn.startsWith("FaceRecognizer") || fn.startsWith("DL4J") || fn.startsWith("FaceTraining") || fn.startsWith("Tesseract") || fn.startsWith("SimpleBlobDetector") || fn.startsWith("Solr") || fn.startsWith("Split")) {
+        log.warn("skipping {}", fn);
         continue;
       }
       cv.addFilter(fn);

From d6c0831f8994b58b95e8bd1b0daa6f944f1258d9 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 10 Feb 2024 17:12:13 -0800
Subject: [PATCH 039/131] cleaned up InMoov2 parts

---
 .../org/myrobotlab/service/InMoov2Arm.java    | 54 +++++++++++++++--
 .../org/myrobotlab/service/InMoov2Hand.java   | 44 ++++++++++++++
 .../org/myrobotlab/service/InMoov2Head.java   | 60 +++++++++++++++++++
 .../org/myrobotlab/service/InMoov2Torso.java  | 33 ++++++++++
 4 files changed, 187 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
index 3b74a3bf20..1b4b45e6b5 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
@@ -10,7 +10,9 @@
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.kinematics.DHLink;
 import org.myrobotlab.kinematics.DHRobotArm;
+import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.math.MathUtils;
 import org.myrobotlab.service.config.InMoov2ArmConfig;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -127,10 +129,18 @@ public void startService() {
   @Override
   public void stopService() {
     super.stopService();
-    releasePeer("bicep");
-    releasePeer("rotate");
-    releasePeer("shoulder");
-    releasePeer("omoplate");
+    if (bicep != null) {
+      ((Service)bicep).stopService();
+    }
+    if (rotate != null) {
+      ((Service)rotate).stopService();
+    }
+    if (shoulder != null) {
+      ((Service)shoulder).stopService();
+    }
+    if (omoplate != null) {
+      ((Service)omoplate).stopService();
+    }
   }
 
   @Override
@@ -296,6 +306,20 @@ public void onJointAngles(Map<String, Double> angleMap) {
   public void releaseService() {
     try {
       disable();
+      
+      if (bicep != null) {
+        ((Service)bicep).releaseService();
+      }
+      if (rotate != null) {
+        ((Service)rotate).releaseService();
+      }
+      if (shoulder != null) {
+        ((Service)shoulder).releaseService();
+      }
+      if (omoplate != null) {
+        ((Service)omoplate).releaseService();
+      }
+      
       super.releaseService();
     } catch (Exception e) {
       error(e);
@@ -468,5 +492,27 @@ public void waitTargetPos() {
     if (omoplate != null)
       omoplate.waitTargetPos();
   }
+  
+  public static void main(String[] args) {
+    LoggingFactory.init(Level.INFO);
+
+    try {
+
+      Runtime.main(new String[] { "--log-level", "info", "-s", "inmoov2arm", "InMoov2Arm" });
+      // Runtime.main(new String[] {});
+      // Runtime.main(new String[] { "--install" });
+      InMoov2Arm arm = (InMoov2Arm)Runtime.start("inmoov2arm", "InMoov2Arm");
+      arm.releaseService();
+
+      boolean done = true;
+      if (done) {
+        return;
+      }
+      log.info("leaving main");
+
+    } catch (Exception e) {
+      log.error("main threw", e);
+    }
+  }
 
 }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
index b30c2bd792..06b6d3b459 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
@@ -570,11 +570,55 @@ public List<String> refreshControllers() {
   public void release() {
     disable();
   }
+  
+  @Override
+  public void stopService() {
+    disable();
+    if (thumb != null) {
+      ((Service)thumb).stopService();
+    }
+    if (index != null) {
+      ((Service)index).stopService();
+    }
+    if (majeure != null) {
+      ((Service)majeure).stopService();
+    }
+    if (ringFinger != null) {
+      ((Service)ringFinger).stopService();
+    }
+    if (pinky != null) {
+      ((Service)pinky).stopService();
+    }
+    if (wrist != null) {
+      ((Service)wrist).stopService();
+    }
+    super.stopService();
+  }
 
   @Override
   public void releaseService() {
     try {
       disable();
+      
+      if (thumb != null) {
+        ((Service)thumb).releaseService();
+      }
+      if (index != null) {
+        ((Service)index).releaseService();
+      }
+      if (majeure != null) {
+        ((Service)majeure).releaseService();
+      }
+      if (ringFinger != null) {
+        ((Service)ringFinger).releaseService();
+      }
+      if (pinky != null) {
+        ((Service)pinky).releaseService();
+      }
+      if (wrist != null) {
+        ((Service)wrist).releaseService();
+      }
+      
       super.releaseService();
     } catch (Exception e) {
       error(e);
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java
index f3f5edf366..da9052ca65 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Head.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java
@@ -359,10 +359,70 @@ public void waitTargetPos() {
   public void release() {
     disable();
   }
+  
+  @Override
+  public void stopService() {
+
+    if (jaw != null) {
+      ((Service)jaw).stopService();
+    }
+    if (eyeX != null) {
+      ((Service)eyeX).stopService();
+    }
+    if (eyeY != null) {
+      ((Service)eyeY).stopService();
+    }
+    if (neck != null) {
+      ((Service)neck).stopService();
+    }
+    if (rothead != null) {
+      ((Service)rothead).stopService();
+    }
+    if (rollNeck != null) {
+      ((Service)rollNeck).stopService();
+    }
+    if (eyelidLeft != null) {
+      ((Service)eyelidLeft).stopService();
+    }
+    if (eyelidRight != null) {
+      ((Service)eyelidRight).stopService();
+    }
+    
+    super.stopService();
+  }
+  
+  
+  
 
   @Override
   public void releaseService() {
     disable();
+    
+    if (jaw != null) {
+      ((Service)jaw).releaseService();
+    }
+    if (eyeX != null) {
+      ((Service)eyeX).releaseService();
+    }
+    if (eyeY != null) {
+      ((Service)eyeY).releaseService();
+    }
+    if (neck != null) {
+      ((Service)neck).releaseService();
+    }
+    if (rothead != null) {
+      ((Service)rothead).releaseService();
+    }
+    if (rollNeck != null) {
+      ((Service)rollNeck).releaseService();
+    }
+    if (eyelidLeft != null) {
+      ((Service)eyelidLeft).releaseService();
+    }
+    if (eyelidRight != null) {
+      ((Service)eyelidRight).releaseService();
+    }
+    
     super.releaseService();
   }
 
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
index 75fa410ca2..3754e43a3c 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
@@ -41,11 +41,44 @@ public void startService() {
     midStom = (ServoControl) getPeer("midStom");
     lowStom = (ServoControl) getPeer("lowStom");
   }
+  
+  @Override
+  public void stopService() {
+    disable();
+    
+    if (topStom != null) {
+      ((Service)topStom).stopService();
+    }
+    
+    if (midStom != null) {
+      ((Service)midStom).stopService();
+    }
+
+    if (lowStom != null) {
+      ((Service)lowStom).stopService();
+    }
+
+    super.stopService();
+  }
 
   @Override
   public void releaseService() {
     try {
       disable();
+      
+      
+      if (topStom != null) {
+        ((Service)topStom).releaseService();
+      }
+      
+      if (midStom != null) {
+        ((Service)midStom).releaseService();
+      }
+
+      if (lowStom != null) {
+        ((Service)lowStom).releaseService();
+      }
+
 
       topStom = null;
       midStom = null;

From cc02708c197635d7b6d4506c262663f43b491fec Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 10 Feb 2024 17:17:39 -0800
Subject: [PATCH 040/131] clean random shutdown

---
 src/main/java/org/myrobotlab/service/Random.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java
index 8a9e1875fb..d018df206a 100644
--- a/src/main/java/org/myrobotlab/service/Random.java
+++ b/src/main/java/org/myrobotlab/service/Random.java
@@ -453,6 +453,12 @@ public void disableAll() {
     }
     broadcastState();
   }
+  
+  @Override
+  public void releaseService() {
+    disable();
+    super.releaseService();
+  }
 
   public static void main(String[] args) {
     try {

From ab0141b771ed110bb2c66046c313020d85ff9bce Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 11 Feb 2024 07:16:12 -0800
Subject: [PATCH 041/131] drupp neck config

---
 .../org/myrobotlab/service/DruppNeck.java     | 33 +++++++++----------
 .../service/config/DruppNeckConfig.java       |  6 ++++
 2 files changed, 21 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/DruppNeck.java b/src/main/java/org/myrobotlab/service/DruppNeck.java
index 1c7aa42e39..e99a1a290c 100755
--- a/src/main/java/org/myrobotlab/service/DruppNeck.java
+++ b/src/main/java/org/myrobotlab/service/DruppNeck.java
@@ -4,7 +4,7 @@
 import org.myrobotlab.kinematics.DruppIKSolver;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.math.MathUtils;
-import org.myrobotlab.service.config.ServiceConfig;
+import org.myrobotlab.service.config.DruppNeckConfig;
 import org.myrobotlab.service.interfaces.ServoControl;
 
 /**
@@ -18,18 +18,14 @@
  * @author kwatters
  *
  */
-public class DruppNeck extends Service<ServiceConfig> {
+public class DruppNeck extends Service<DruppNeckConfig> {
 
   private static final long serialVersionUID = 1L;
   // 3 servos for the drupp neck
-  public transient ServoControl up;
-  public transient ServoControl middle;
-  public transient ServoControl down;
+  protected transient ServoControl up;
+  protected transient ServoControl middle;
+  protected transient ServoControl down;
 
-  // this is an offset angle that is added to the solution from the IK solver
-  public double upOffset = 90;
-  public double middleOffset = 120 + 90;
-  public double downOffset = -120 + 90;
 
   public DruppNeck(String n, String id) {
     super(n, id);
@@ -61,9 +57,9 @@ public void moveTo(double roll, double pitch, double yaw) throws Exception {
     // TODO: if the solver fails, should we catch this exception ?
     double[] result = solver.solve(rollRad, pitchRad, yawRad);
     // convert to degrees
-    double upDeg = MathUtils.radToDeg(result[0]) + upOffset;
-    double middleDeg = MathUtils.radToDeg(result[1]) + middleOffset;
-    double downDeg = MathUtils.radToDeg(result[2]) + downOffset;
+    double upDeg = MathUtils.radToDeg(result[0]) + config.upOffset;
+    double middleDeg = MathUtils.radToDeg(result[1]) + config.middleOffset;
+    double downDeg = MathUtils.radToDeg(result[2]) + config.downOffset;
     // Ok, servos can only (typically) move from 0 to 180.. if any of the angles
     // are
     // negative... we can't move there.. let's log a warning
@@ -84,6 +80,7 @@ public void moveTo(double roll, double pitch, double yaw) throws Exception {
     down.moveTo(downDeg);
     // TODO: broadcast state?
   }
+  
 
   /**
    * Enable the servos
@@ -141,27 +138,27 @@ public void setServos(ServoControl up, ServoControl middle, ServoControl down) {
   }
 
   public double getUpOffset() {
-    return upOffset;
+    return config.upOffset;
   }
 
   public void setUpOffset(double upOffset) {
-    this.upOffset = upOffset;
+    this.config.upOffset = upOffset;
   }
 
   public double getMiddleOffset() {
-    return middleOffset;
+    return config.middleOffset;
   }
 
   public void setMiddleOffset(double middleOffset) {
-    this.middleOffset = middleOffset;
+    this.config.middleOffset = middleOffset;
   }
 
   public double getDownOffset() {
-    return downOffset;
+    return config.downOffset;
   }
 
   public void setDownOffset(double downOffset) {
-    this.downOffset = downOffset;
+    this.config.downOffset = downOffset;
   }
 
   public static void main(String[] args) throws Exception {
diff --git a/src/main/java/org/myrobotlab/service/config/DruppNeckConfig.java b/src/main/java/org/myrobotlab/service/config/DruppNeckConfig.java
index d4b3f730fb..17fc10efb9 100644
--- a/src/main/java/org/myrobotlab/service/config/DruppNeckConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/DruppNeckConfig.java
@@ -3,6 +3,12 @@
 import org.myrobotlab.framework.Plan;
 
 public class DruppNeckConfig extends ServiceConfig {
+  
+  // this is an offset angle that is added to the solution from the IK solver
+  public double upOffset = 90;
+  public double middleOffset = 120 + 90;
+  public double downOffset = -120 + 90;
+
 
   @Override
   public Plan getDefault(Plan plan, String name) {

From 0bd316b53f66e02f1ec4737dc7d566fba11dbb7c Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 11 Feb 2024 08:49:53 -0800
Subject: [PATCH 042/131] fixed so Runtime.release(name) and
 Service.releaseService(name) behave consistently

---
 .../org/myrobotlab/framework/Service.java     |  2 +-
 .../org/myrobotlab/service/DruppNeck.java     | 43 +++++++++++--------
 .../java/org/myrobotlab/service/Runtime.java  | 17 ++++++++
 3 files changed, 43 insertions(+), 19 deletions(-)

diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 0449c05e16..3f7acbf755 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -1608,7 +1608,7 @@ public Service<T> publishState() {
   @Override
   synchronized public void releaseService() {
     // auto release children and unregister
-    Runtime.releaseService(getName());
+    Runtime.releaseServiceInternal(getName());
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/service/DruppNeck.java b/src/main/java/org/myrobotlab/service/DruppNeck.java
index e99a1a290c..fbf5cd3a11 100755
--- a/src/main/java/org/myrobotlab/service/DruppNeck.java
+++ b/src/main/java/org/myrobotlab/service/DruppNeck.java
@@ -30,6 +30,13 @@ public class DruppNeck extends Service<DruppNeckConfig> {
   public DruppNeck(String n, String id) {
     super(n, id);
   }
+  
+  public void startService() {
+    super.startService();
+    up = (ServoControl)startPeer("up");
+    middle = (ServoControl)startPeer("middle");
+    down = (ServoControl)startPeer("down");
+  }
 
   private DruppIKSolver solver = new DruppIKSolver();
 
@@ -165,26 +172,26 @@ public static void main(String[] args) throws Exception {
     LoggingFactory.init("INFO");
     // To use the drup service you need to configure and attach the servos
     // then set them on the service.
-    Runtime.start("gui", "SwingGui");
-    Runtime.start("python", "Python");
-    Servo up = (Servo) Runtime.start("up", "Servo");
-    Servo middle = (Servo) Runtime.start("middle", "Servo");
-    Servo down = (Servo) Runtime.start("down", "Servo");
-    up.setPin(6);
-    middle.setPin(5);
-    down.setPin(4);
-    // String port = "COM4";
-    String port = "VIRTUAL_COM_PORT";
-    VirtualArduino va1 = (VirtualArduino) Runtime.start("va1", "VirtualArduino");
-    va1.connect(port);
-    Arduino ard = (Arduino) Runtime.start("ard", "Arduino");
-    ard.connect(port);
-    ard.attach(up);
-    ard.attach(middle);
-    ard.attach(down);
+//    Runtime.start("python", "Python");
+//    Servo up = (Servo) Runtime.start("up", "Servo");
+//    Servo middle = (Servo) Runtime.start("middle", "Servo");
+//    Servo down = (Servo) Runtime.start("down", "Servo");
+//    up.setPin(6);
+//    middle.setPin(5);
+//    down.setPin(4);
+//    // String port = "COM4";
+//    String port = "VIRTUAL_COM_PORT";
+//    VirtualArduino va1 = (VirtualArduino) Runtime.start("va1", "VirtualArduino");
+//    va1.connect(port);
+//    Arduino ard = (Arduino) Runtime.start("ard", "Arduino");
+//    ard.connect(port);
+//    ard.attach(up);
+//    ard.attach(middle);
+//    ard.attach(down);
     // Create the drupp service
     DruppNeck neck = (DruppNeck) Runtime.start("neck", "DruppNeck");
-    neck.setServos(up, middle, down);
+    Runtime.start("webgui", "WebGui");
+    // neck.setServos(up, middle, down);
     // neck.moveTo(0, 0, 0);
     // neck.moveTo(0, 0, -45);
     // neck.moveTo(0, 0, 45);
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 9902b3a17b..e838cf6c52 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -1933,6 +1933,22 @@ public static Registration register(Registration registration) {
    *
    */
   public static boolean releaseService(String inName) {
+    ServiceInterface sc = getService(inName);
+    if (sc != null) {
+      sc.releaseService();
+      return true;
+    }
+    return false;
+  }
+  
+  
+  /**
+   * Called after any subclassed releaseService has been called, this cleans
+   * up the registry and removes peers
+   * @param inName
+   * @return
+   */
+  public static boolean releaseServiceInternal(String inName) {
     synchronized (processLock) {
       if (inName == null) {
         log.debug("release (null)");
@@ -1988,6 +2004,7 @@ public static boolean releaseService(String inName) {
     }
   }
 
+
   /**
    * Removes registration for a service. Removes the service from
    * {@link #typeToInterface} and {@link #interfaceToNames}.

From 3fb04c8bdb3a7b449a70b17a615f6fdd03bd5fdf Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 11 Feb 2024 11:38:32 -0800
Subject: [PATCH 043/131] drupp merge

---
 .../java/org/myrobotlab/service/DruppNeck.java  | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/DruppNeck.java b/src/main/java/org/myrobotlab/service/DruppNeck.java
index 9e7f11bf25..b07c42419b 100755
--- a/src/main/java/org/myrobotlab/service/DruppNeck.java
+++ b/src/main/java/org/myrobotlab/service/DruppNeck.java
@@ -45,13 +45,13 @@ public void startService() {
    * down servos.
    * 
    * @param roll
-   *              degrees
+   *          degrees
    * @param pitch
-   *              degrees
+   *          degrees
    * @param yaw
-   *              degrees
+   *          degrees
    * @throws Exception
-   *                   boom
+   *           boom
    * 
    */
   public void moveTo(double roll, double pitch, double yaw) throws Exception {
@@ -74,14 +74,12 @@ public void moveTo(double roll, double pitch, double yaw) throws Exception {
     // but for the drupp neck, if you've installed it correctly,
     // all servos can go from 0 to 180...
     if (upDeg < 0 || middleDeg < 0 || downDeg < 0 || upDeg > 180 || middleDeg > 180 || downDeg > 180) {
-      log.warn("Target Position out of range! {} Pitch {} Yaw {} -> Up {} Middle {} Down {}", roll, pitch, yaw,
-          MathUtils.round(upDeg, 3), MathUtils.round(middleDeg, 3),
+      log.warn("Target Position out of range! {} Pitch {} Yaw {} -> Up {} Middle {} Down {}", roll, pitch, yaw, MathUtils.round(upDeg, 3), MathUtils.round(middleDeg, 3),
           MathUtils.round(downDeg, 3));
       // Skipping this movement as it's likely unstable!
       return;
     }
-    log.info("Input Roll {} Pitch {} Yaw {} -> Up {} Middle {} Down {}", roll, pitch, yaw, MathUtils.round(upDeg, 3),
-        MathUtils.round(middleDeg, 3), MathUtils.round(downDeg, 3));
+    log.info("Input Roll {} Pitch {} Yaw {} -> Up {} Middle {} Down {}", roll, pitch, yaw, MathUtils.round(upDeg, 3), MathUtils.round(middleDeg, 3), MathUtils.round(downDeg, 3));
     // we should probably track the last moved to position.
     up.moveTo(upDeg);
     middle.moveTo(middleDeg);
@@ -181,7 +179,8 @@ public static void main(String[] args) throws Exception {
     // down.setPin(4);
     // // String port = "COM4";
     // String port = "VIRTUAL_COM_PORT";
-    // VirtualArduino va1 = (VirtualArduino) Runtime.start("va1", "VirtualArduino");
+    // VirtualArduino va1 = (VirtualArduino) Runtime.start("va1",
+    // "VirtualArduino");
     // va1.connect(port);
     // Arduino ard = (Arduino) Runtime.start("ard", "Arduino");
     // ard.connect(port);

From b70114e169ec0a6112ad9e3d16e0f8bf53da5366 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 11 Feb 2024 17:51:11 -0800
Subject: [PATCH 044/131] intermediate

---
 .../org/myrobotlab/service/JMonkeyEngine.java |  2 +-
 .../org/myrobotlab/service/MotorDualPwm.java  |  2 +-
 .../java/org/myrobotlab/service/OpenCV.java   |  6 ++
 .../java/org/myrobotlab/service/Python.java   |  2 +-
 .../java/org/myrobotlab/service/Random.java   |  6 ++
 .../java/org/myrobotlab/service/Serial.java   |  2 +-
 .../service/config/OpenWeatherMapConfig.java  |  2 +-
 .../config/YahooFinanceStockQuoteConfig.java  |  2 +-
 .../myrobotlab/service/meta/JoystickMeta.java |  6 +-
 .../myrobotlab/vertx/WebSocketHandler.java    |  2 +-
 .../app/service/js/FiniteStateMachineGui.js   |  1 +
 .../service/views/FiniteStateMachineGui.html  | 17 +++++
 .../myrobotlab/programab/TemplateTest.java    | 52 +++++++++++++++
 .../org/myrobotlab/test/AbstractTest.java     | 64 +++++++++++++++++--
 14 files changed, 150 insertions(+), 16 deletions(-)
 create mode 100644 src/test/java/org/myrobotlab/programab/TemplateTest.java

diff --git a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
index 2d19048c84..af2d354f43 100644
--- a/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
+++ b/src/main/java/org/myrobotlab/service/JMonkeyEngine.java
@@ -2489,7 +2489,7 @@ public static void main(String[] args) {
         i01.startPeer("simulator");
       }
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
       // Runtime.main(new String[] { "--interactive", "--id", "admin" });
       JMonkeyEngine jme = (JMonkeyEngine) Runtime.start("simulator", "JMonkeyEngine");
 
diff --git a/src/main/java/org/myrobotlab/service/MotorDualPwm.java b/src/main/java/org/myrobotlab/service/MotorDualPwm.java
index e1746ca071..3187e52ee0 100644
--- a/src/main/java/org/myrobotlab/service/MotorDualPwm.java
+++ b/src/main/java/org/myrobotlab/service/MotorDualPwm.java
@@ -96,7 +96,7 @@ public static void main(String[] args) {
       LoggingFactory.init(Level.INFO);
       String arduinoPort = "COM5";
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
       Runtime.startConfig("dev");
       Runtime.start("webgui", "WebGui");
       MotorDualPwm motor = (MotorDualPwm) Runtime.start("motor", "MotorDualPwm");
diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java
index ff093721bd..dcd7ad08d9 100644
--- a/src/main/java/org/myrobotlab/service/OpenCV.java
+++ b/src/main/java/org/myrobotlab/service/OpenCV.java
@@ -461,6 +461,7 @@ public void reset() {
     singleFrame = false;
     lastFrame = null;
     blockingData.clear();
+    removeFilters();
   }
 
   public static IplImage cropImage(IplImage img, CvRect rect) {
@@ -2099,6 +2100,11 @@ public static void main(String[] args) throws Exception {
 
       // Runtime.start("python", "Python");
       OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV");
+      cv.capture();
+      
+      cv.addFilter(new OpenCVFilterYolo("yolo"));
+      sleep(1000);
+      cv.removeFilters();
 
       OpenCVFilter fr = new OpenCVFilterFaceRecognizer("fr");
       cv.addFilter(fr);
diff --git a/src/main/java/org/myrobotlab/service/Python.java b/src/main/java/org/myrobotlab/service/Python.java
index 7700a1318d..c3d18162ed 100644
--- a/src/main/java/org/myrobotlab/service/Python.java
+++ b/src/main/java/org/myrobotlab/service/Python.java
@@ -679,7 +679,7 @@ public void onStarted(String serviceName) {
 
   @Override
   public void onReleased(String serviceName) {
-    String registerScript = String.format("%s = None\n", CodecUtils.getSafeReferenceName(serviceName));
+    String registerScript = String.format("%s = None\n", CodecUtils.getSafeReferenceName(CodecUtils.getShortName(serviceName)));
     exec(registerScript, false);
   }
 
diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java
index 8a9e1875fb..d018df206a 100644
--- a/src/main/java/org/myrobotlab/service/Random.java
+++ b/src/main/java/org/myrobotlab/service/Random.java
@@ -453,6 +453,12 @@ public void disableAll() {
     }
     broadcastState();
   }
+  
+  @Override
+  public void releaseService() {
+    disable();
+    super.releaseService();
+  }
 
   public static void main(String[] args) {
     try {
diff --git a/src/main/java/org/myrobotlab/service/Serial.java b/src/main/java/org/myrobotlab/service/Serial.java
index af16f3522a..c6fccb9d84 100644
--- a/src/main/java/org/myrobotlab/service/Serial.java
+++ b/src/main/java/org/myrobotlab/service/Serial.java
@@ -1338,7 +1338,7 @@ public static void main(String[] args) {
 
     try {
 
-      Platform.setVirtual(true);
+      Runtime.getInstance().setVirtual(true);
 
       Serial s = (Serial) Runtime.start("s1", "Serial");
       String vport1 = "vport1";
diff --git a/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java b/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
index 37cdd82a0b..9a6a51baab 100644
--- a/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/OpenWeatherMapConfig.java
@@ -3,7 +3,7 @@
 import org.myrobotlab.framework.Peer;
 import org.myrobotlab.framework.Plan;
 
-public class OpenWeatherMapConfig extends ServiceConfig {
+public class OpenWeatherMapConfig extends HttpClientConfig {
 
   public String currentUnits;
   public String currentTown;
diff --git a/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java b/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
index d351154c44..32ae5984d8 100644
--- a/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/YahooFinanceStockQuoteConfig.java
@@ -1,5 +1,5 @@
 package org.myrobotlab.service.config;
 
-public class YahooFinanceStockQuoteConfig extends ServiceConfig {
+public class YahooFinanceStockQuoteConfig extends HttpClientConfig {
 
 }
diff --git a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
index ca544c8523..b2fb538f80 100644
--- a/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/JoystickMeta.java
@@ -20,12 +20,12 @@ public JoystickMeta() {
     addCategory("control", "telerobotics");
     addDependency("net.java.jinput", "jinput", "2.0.9");
 
-    log.info("Joystick.getMetaData {}  isArm() {}", platform, platform.isArm());
+    log.debug("Joystick.getMetaData {}  isArm() {}", platform, platform.isArm());
     if (platform.isArm()) {
-      log.info("adding armv7 native dependencies");
+      log.debug("adding armv7 native dependencies");
       addDependency("jinput-natives", "jinput-natives-armv7.hfp", "2.0.7", "zip");
     } else {
-      log.info("adding jinput native dependencies");
+      log.debug("adding jinput native dependencies");
       addDependency("jinput-natives", "jinput-natives", "2.0.7", "zip");
     }
   }
diff --git a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
index e7e6b5fe63..c74b12e185 100644
--- a/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
+++ b/src/main/java/org/myrobotlab/vertx/WebSocketHandler.java
@@ -131,7 +131,7 @@ public void handle(String json) {
         // FIXME get rid of fill-uuid
         Message describe = Message.createMessage(String.format("%s@%s", service.getName(), Runtime.get().getId()),
             "runtime", "describe",
-            new Object[] { "fill-uuid", new DescribeQuery(Platform.getLocalInstance().getId(), uuid) });
+            new Object[] { "fill-uuid", new DescribeQuery(Runtime.getInstance().getId(), uuid) });
         service.sendRemote(describe);
         log.info(String.format("<-- %s", describe));
         newConnection = false;
diff --git a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
index 42c559c999..d08f8f2fc8 100644
--- a/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/FiniteStateMachineGui.js
@@ -51,6 +51,7 @@ angular.module('mrlapp.service.FiniteStateMachineGui', []).controller('FiniteSta
             break
         case 'onStateChange':
             $scope.current = data.current
+            $scope.service.history.push(data)
             $scope.$apply()
             break
         default:
diff --git a/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html b/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
index 067ee7624c..b61f5af303 100644
--- a/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/FiniteStateMachineGui.html
@@ -54,5 +54,22 @@ <h3>Last Event {{event}} Current State: {{current}}</h3>
                 <td></td>
             </tr>
         </table>
+<table class="table-condensed table-striped table-bordered">
+  <thead>
+    <tr>
+      <th>Timestamp</th>
+      <th>State</th>
+      <th>Event</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="item in service.history">
+      <td>{{ item.ts }}</td>
+      <td>{{ item.state }}</td>
+      <td>{{ item.event }}</td>
+    </tr>
+  </tbody>
+</table>            
+            
     </div>
 </div>
diff --git a/src/test/java/org/myrobotlab/programab/TemplateTest.java b/src/test/java/org/myrobotlab/programab/TemplateTest.java
new file mode 100644
index 0000000000..8d2beb245d
--- /dev/null
+++ b/src/test/java/org/myrobotlab/programab/TemplateTest.java
@@ -0,0 +1,52 @@
+package org.myrobotlab.programab;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.programab.models.Mrl;
+import org.myrobotlab.programab.models.Template;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+public class TemplateTest {
+
+  public final static Logger log = LoggerFactory.getLogger(TemplateTest.class);
+
+  @Test
+  public void testXmlParsing() {
+    try {
+
+      String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method><param>p1</param><param>p2</param><param>p3</param></mrl><mrl><service>blah2</service><method>method2</method></mrl><mrljson>[\"method\":\"doIt\",\"data\":[\"p1\"]]</mrljson></oob></template>";
+
+      XmlMapper xmlMapper = new XmlMapper();
+      Template template = xmlMapper.readValue(xml, Template.class);
+
+      assertNotNull(template);
+      assertEquals("XXXX", template.text);
+
+      // Verify Oob parsing
+      assertNotNull(template.oob);
+      assertEquals(2, template.oob.mrl.size());
+
+      // Verify the first Mrl
+      Mrl mrl1 = template.oob.mrl.get(0);
+      assertEquals("blah1", mrl1.service);
+      assertEquals("method1", mrl1.method);
+      assertEquals(3, mrl1.params.size());
+
+      // Verify the second Mrl
+      Mrl mrl2 = template.oob.mrl.get(1);
+      assertEquals("blah2", mrl2.service);
+      assertEquals("method2", mrl2.method);
+      assertNull(mrl2.params);
+
+    } catch (Exception e) {
+      fail("Exception occurred: " + e.getMessage());
+    }
+  }
+}
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index c4fc33b83d..d782797c9b 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -1,5 +1,7 @@
 package org.myrobotlab.test;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -13,12 +15,19 @@
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.rules.TestName;
+import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.RuntimeConfig;
 import org.slf4j.Logger;
 
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
 public class AbstractTest {
 
   /** cached network test value for tests */
@@ -44,6 +53,34 @@ public class AbstractTest {
   static public String simpleName;
 
   private static boolean lineFeedFooter = true;
+  
+  @Rule
+  public TestWatcher watchman = new TestWatcher() {
+      @Override
+      protected void starting(Description description) {
+          System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
+      }
+
+      @Override
+      protected void succeeded(Description description) {
+         // System.out.println("Succeeded: " + description.getMethodName());
+      }
+
+      @Override
+      protected void failed(Throwable e, Description description) {
+          System.out.println("Failed: " + description.getMethodName());
+      }
+
+      @Override
+      protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
+          System.out.println("Skipped: " + description.getMethodName());
+      }
+
+      @Override
+      protected void finished(Description description) {
+          System.out.println("Finished: " + description.getMethodName());
+      }
+  };
 
   public String getSimpleName() {
     return simpleName;
@@ -83,8 +120,23 @@ public static void main(String[] args) {
 
   @BeforeClass
   public static void setUpAbstractTest() throws Exception {
-
-    Platform.setVirtual(true);
+    
+    // setup runtime resource = src/main/resources/resource
+    File runtimeYml = new File("data/config/default/runtime.yml");
+//    if (!runtimeYml.exists()) {
+      runtimeYml.getParentFile().mkdirs();
+      RuntimeConfig rc = new RuntimeConfig();
+      rc.resource = "src/main/resources/resource";
+      String yml = CodecUtils.toYaml(rc);
+      
+      FileOutputStream fos = null;
+      fos = new FileOutputStream(runtimeYml);
+      fos.write(yml.getBytes());
+      fos.close();
+      
+//    }
+
+      Runtime.getInstance().setVirtual(true);
 
     String junitLogLevel = System.getProperty("junit.logLevel");
     if (junitLogLevel != null) {
@@ -145,7 +197,7 @@ static protected void installAll() {
    */
   public static void releaseServices() {
 
-    log.info("end of test - id {} remaining services {}", Platform.getLocalInstance().getId(),
+    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(),
         Arrays.toString(Runtime.getServiceNames()));
 
     // release all including runtime - be careful of default runtime.yml
@@ -176,7 +228,7 @@ public static void releaseServices() {
       }
     }
     if (threadsRemaining.size() > 0) {
-      log.info("{} straggling threads remain [{}]", threadsRemaining.size(), String.join(",", threadsRemaining));
+      log.warn("{} straggling threads remain [{}]", threadsRemaining.size(), String.join(",", threadsRemaining));
     }
 
     // log.warn("end of test - id {} remaining services after release {}",
@@ -192,11 +244,11 @@ public AbstractTest() {
   }
 
   public void setVirtual() {
-    Platform.setVirtual(true);
+    Runtime.getInstance().setVirtual(true);
   }
 
   public boolean isVirtual() {
-    return Platform.isVirtual();
+    return Runtime.getInstance().isVirtual();
   }
 
 }

From abd63e2143779d5d337010c0ca05b11e9c1d27fd Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 12 Feb 2024 05:59:34 -0800
Subject: [PATCH 045/131] javadoc and other small changes

---
 .../java/org/myrobotlab/codec/CodecUtils.java |   4 +-
 .../org/myrobotlab/config/ConfigUtils.java    | 142 +++++++++++++-----
 .../framework/repo/MavenWrapper.java          |   6 +-
 .../org/myrobotlab/opencv/OpenCVFilter.java   |   2 +-
 .../opencv/OpenCVFilterMiniXception.java      |   5 +-
 .../myrobotlab/opencv/OpenCVFilterYolo.java   |   4 +-
 .../myrobotlab/programab/models/Event.java    |  24 +--
 .../org/myrobotlab/programab/models/Oob.java  |  12 +-
 .../myrobotlab/programab/models/Sraix.java    |  16 +-
 .../myrobotlab/programab/models/Template.java |  45 ++----
 .../service/interfaces/Gateway.java           |   2 +-
 .../myrobotlab/config/ConfigUtilsTest.java    |  43 ++++++
 .../org/myrobotlab/service/OpenCVTest.java    |  11 +-
 13 files changed, 219 insertions(+), 97 deletions(-)
 create mode 100644 src/test/java/org/myrobotlab/config/ConfigUtilsTest.java

diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java
index 372caa474b..09d2086c9e 100644
--- a/src/main/java/org/myrobotlab/codec/CodecUtils.java
+++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java
@@ -495,7 +495,7 @@ public static String getFullName(String name) {
     }
 
     if (getId(name) == null) {
-      return name + '@' + Platform.getLocalInstance().getId();
+      return name + '@' + Runtime.getInstance().getId();
     } else {
       return name;
     }
@@ -1466,7 +1466,7 @@ public static boolean isLocal(String name) {
     if (!name.contains("@")) {
       return true;
     }
-    return name.substring(name.indexOf("@") + 1).equals(Platform.getLocalInstance().getId());
+    return name.substring(name.indexOf("@") + 1).equals(Runtime.getInstance().getId());
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 35c8a776a8..19c256a8cf 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -4,13 +4,26 @@
 import java.io.IOException;
 
 import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.framework.CmdOptions;
 import org.myrobotlab.framework.StartYml;
 import org.myrobotlab.io.FileIO;
+import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
 import org.myrobotlab.service.config.RuntimeConfig;
+import org.slf4j.Logger;
 
+/**
+ * Class to process basic configuration functions and processing.
+ * 
+ * @author GroG
+ *
+ */
 public class ConfigUtils {
 
+  public final static Logger log = LoggerFactory.getLogger(Runtime.class);
+
+  private static RuntimeConfig config;
+
   /**
    * This gets the current resource root without starting a Runtime instance if
    * not already started. The resource root depends on config, if Runtime is
@@ -21,51 +34,100 @@ public class ConfigUtils {
    * @return
    */
   public static String getResourceRoot() {
+    if (config == null) {
+      loadRuntimeConfig(null);
+    }
+    return config.resource;
+
+  }
+
+  /**
+   * Loads a runtime config based on the configName. config =
+   * data/config/{configName}/runtime.yml If one does exits, it is returned, if
+   * one does not exist a default one is created and saved.
+   * 
+   * @param configName
+   * @return
+   */
+  static public RuntimeConfig loadRuntimeConfig(CmdOptions options) {
+
+    if (config != null) {
+      return config;
+    }
 
-    String resource = "resource";
+    StartYml startYml = loadStartYml();
+    String configName = null;
 
-    // check if runtime is running
-    if (!Runtime.isAvailable()) {
-      // check for start.yml
+    if (startYml.enable) {
+      configName = startYml.config;
+    }
+
+    // start with default
+    config = new RuntimeConfig();
+    try {
 
-      File checkStartYml = new File("start.yml");
-      StartYml startYml = new StartYml();
-      if (checkStartYml.exists()) {
-        String yml;
+      File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + configName + File.separator + "runtime.yml");
+      if (runtimeYml.exists()) {
+        // parse that file look for resource: entry in file
+        config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
+      } else {
+        FileIO.toFile(runtimeYml, CodecUtils.toYaml(config).getBytes());
+      }
+
+    } catch (IOException e) {
+      log.error("loadRuntimeConfig threw", e);
+    }
+
+    if (options != null && options.id != null) {
+      config.id = options.id;
+    }
+
+    return config;
+  }
+
+  public static StartYml loadStartYml() {
+    StartYml startYml = new StartYml();
+    String defaultStartFile = CodecUtils.toYaml(startYml);
+    File checkStartYml = new File("start.yml");
+    if (!checkStartYml.exists()) {
+      // save default start.yml
+      startYml = new StartYml();
+      try {
+        FileIO.toFile("start.yml", defaultStartFile);
+      } catch (IOException e) {
+        log.error("could not save start.yml");
+      }
+    } else {
+      // load start.yml
+      try {
+        String yml = FileIO.toString("start.yml");
+        startYml = CodecUtils.fromYaml(yml, StartYml.class);
+      } catch (Exception e) {
+        log.error("could not load start.yml replacing with new start.yml", e);
+        startYml = new StartYml();
         try {
-          yml = FileIO.toString("start.yml");
-          startYml = CodecUtils.fromYaml(yml, StartYml.class);
-
-          // see if autostart is on with a config
-          if (startYml.enable) {
-            // use that config to find runtime.yml
-
-            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + startYml.config + File.separator + "runtime.yml");
-            if (runtimeYml.exists()) {
-              // parse that file look for resource: entry in file
-              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
-              resource = config.resource;
-            }
-
-          } else {
-            // start.yml enable = false / so we'll use default config
-            File runtimeYml = new File(Runtime.ROOT_CONFIG_DIR + File.separator + "default" + File.separator + "runtime.yml");
-            if (runtimeYml.exists()) {
-              // parse that file look for resource: entry in file
-              RuntimeConfig config = (RuntimeConfig) CodecUtils.readServiceConfig(runtimeYml.getAbsolutePath());
-              resource = config.resource;
-            }
-          }
-
-        } catch (IOException e) {
-          // problem getting or parsing
-          // going to assume default "resource"
+          FileIO.toFile("start.yml", defaultStartFile);
+        } catch (IOException ex) {
+          log.error("could not save start.yml", ex);
         }
-      } // no startYml
-      return resource;
-    } else {
-      // Runtime is available - ask it
-      return Runtime.getInstance().getConfig().resource;
+      }
     }
+    log.info("start.yml exists {} {}", checkStartYml.exists(), CodecUtils.toJson(startYml));
+    return startYml;
   }
+
+  public static String getId() {
+    if (config == null) {
+      loadRuntimeConfig(null);
+    }
+    return config.id;
+  }
+
+  /**
+   * If Runtime.releaseAll is called the statics here should be reset
+   */
+  public static void reset() {
+    config = null;
+  }
+
 }
diff --git a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
index 8374f92cdf..62c0027cbd 100644
--- a/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
+++ b/src/main/java/org/myrobotlab/framework/repo/MavenWrapper.java
@@ -1,5 +1,5 @@
 package org.myrobotlab.framework.repo;
-
+import org.myrobotlab.service.Runtime;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -281,6 +281,8 @@ public static void main(String[] args) {
 
       LoggingFactory.init(Level.INFO);
       
+      Runtime.getInstance();
+      
       File libraries = new File(ServiceData.LIBRARIES);
       libraries.mkdir();
       File cache = new File(ServiceData.LIBRARIES + File.separator + "serviceData.json");
@@ -309,7 +311,7 @@ public static void main(String[] args) {
       // repo.installTo(dir);
       // repo.install();
       // repo.installEach(); <-- TODO - test
-
+      Runtime.shutdown();
       log.info("done");
 
     } catch (Exception e) {
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
index ac90bae143..860bceadc3 100644
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilter.java
@@ -192,7 +192,7 @@ static private Mat read(String filename) {
   /**
    * This will enable/disable the filter in the pipeline
    */
-  protected boolean enabled = true;
+  protected volatile boolean enabled = true;
 
   protected int height;
 
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
index 8dea3fdf75..bd5de81abb 100755
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterMiniXception.java
@@ -59,7 +59,7 @@ public OpenCVFilterMiniXception(String name) {
   }
 
   private void loadDL4j() {
-    dl4j = (Deeplearning4j) Runtime.createAndStart("dl4j", "Deeplearning4j");
+    dl4j = (Deeplearning4j) Runtime.start("dl4j", "Deeplearning4j");
     log.info("Loading mini XCEPTION Model.");
     try {
       dl4j.loadMiniEXCEPTION();
@@ -158,6 +158,9 @@ public void release() {
     running = false;
     converter1.close();
     converter2.close();
+    if (dl4j != null) {
+      dl4j.releaseService();
+    }
   }
 
   @Override
diff --git a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
index ce83566839..060b5e43ce 100755
--- a/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
+++ b/src/main/java/org/myrobotlab/opencv/OpenCVFilterYolo.java
@@ -378,12 +378,12 @@ public void enable() {
 
   @Override
   public void disable() {
+    super.disable();
     if (classifier == null) {
       // already disabled
       return;
     }
-    super.disable();
-    int waitTime = 0;
+    int waitTime = 0;    
     while (classifier != null && waitTime < 1000) {
       ++waitTime;
       Service.sleep(10);
diff --git a/src/main/java/org/myrobotlab/programab/models/Event.java b/src/main/java/org/myrobotlab/programab/models/Event.java
index 7f1033ccdf..d7aeddd852 100644
--- a/src/main/java/org/myrobotlab/programab/models/Event.java
+++ b/src/main/java/org/myrobotlab/programab/models/Event.java
@@ -2,12 +2,13 @@
 
 /**
  * Pojo for state change of one of ProgramAB's state info
+ * 
  * @author GroG
  *
  */
 public class Event {
   /**
-   * the botName in this state change - typically 
+   * the botName in this state change - typically
    * current session botName
    */
   public String botname;
@@ -15,49 +16,48 @@ public class Event {
    * unique identifier for the session user and bot
    */
   public String id;
-  
+
   /**
    * name of the predicate changed
    */
   public String name;
-    
+
   /**
    * service this topic change came from
    */
   public String src;
-  
+
   /**
    * new topic or state name in this transition
    */
   public String topic;
-   
+
   /**
    * timestamp
    */
   public long ts = System.currentTimeMillis();
-  
+
   /**
    * the user name in this state change - usually
    * current session userName
    */
   public String user;
-  
+
   /**
    * new value
    */
   public String value;
-  
-  public Event() {    
+
+  public Event() {
   }
-  
+
   public Event(String src, String userName, String botName, String topic) {
     this.src = src;
     this.user = userName;
     this.botname = botName;
     this.topic = topic;
   }
-  
-  
+
   @Override
   public String toString() {
     return String.format("%s %s=%s", id, name, value);
diff --git a/src/main/java/org/myrobotlab/programab/models/Oob.java b/src/main/java/org/myrobotlab/programab/models/Oob.java
index 833bab5a0f..5e0d99c4cf 100644
--- a/src/main/java/org/myrobotlab/programab/models/Oob.java
+++ b/src/main/java/org/myrobotlab/programab/models/Oob.java
@@ -4,11 +4,17 @@
 
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 
+/**
+ * AIML 2.0 Oob Out Of Band xml defined with mrl - legacy and mrljson - json
+ * typed message
+ * 
+ * @author GroG
+ *
+ */
 public class Oob {
-  
+
   public String mrljson;
-  
+
   @JacksonXmlElementWrapper(useWrapping = false)
   public List<Mrl> mrl;
 }
-
diff --git a/src/main/java/org/myrobotlab/programab/models/Sraix.java b/src/main/java/org/myrobotlab/programab/models/Sraix.java
index 99b0639cb6..1b130ad805 100644
--- a/src/main/java/org/myrobotlab/programab/models/Sraix.java
+++ b/src/main/java/org/myrobotlab/programab/models/Sraix.java
@@ -1,10 +1,22 @@
 package org.myrobotlab.programab.models;
 
-// FIXME add attributes and internal tags
+/**
+ * Basic Sraix model, AIML 2.0 has more elements but these seemed like the most
+ * relevant and ar actually used.
+ * 
+ * @author GroG
+ *
+ */
 public class Sraix {
 
+  /**
+   * Search text when a query is sent to a remote system
+   */
   public String search;
 
+  /**
+   * Oob is Out Of Band text which can be handled by internal processing
+   */
   public Oob oob;
-  
+
 }
diff --git a/src/main/java/org/myrobotlab/programab/models/Template.java b/src/main/java/org/myrobotlab/programab/models/Template.java
index 91f8e5de51..d657973005 100644
--- a/src/main/java/org/myrobotlab/programab/models/Template.java
+++ b/src/main/java/org/myrobotlab/programab/models/Template.java
@@ -5,47 +5,34 @@
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
 
-//@JacksonXmlRootElement(localName = "template")
-//@JsonIgnoreProperties(ignoreUnknown = true)
+/**
+ * General aiml template used for future parsing
+ * 
+ * @author GroG
+ */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class Template {
-  // @JacksonXmlElementWrapper(useWrapping = false)
-  
+
   @JacksonXmlProperty(localName = "template")
-  
   @JacksonXmlText
-  public String text;  
-  
-  
-public Oob oob;
-  
-//  @JsonProperty("ignorable")
-//  public List<Oob> oob;
-//
-//  public List<Oob> getOob() {
-//    return oob;
-//  }
-//
-//  public void setOob(List<Oob> oob) {
-//    this.oob = oob;
-//  }
-  
+  public String text;
+
+  public Oob oob;
+
   public static void main(String[] args) {
 
     try {
-      
-      // String xml = "<template>XXX<oob><mrl><service>blah</service><method>method</method></mrl></oob></template>";
-      // String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method></mrl><mrl><service>blah2</service><method>method2</method></mrl></oob></template>";
+
       String xml = "<template>XXXX<oob><mrl><service>blah1</service><method>method1</method><param>p1</param><param>p2</param><param>p3</param></mrl><mrl><service>blah2</service><method>method2</method></mrl><mrljson>[\"method\":\"doIt\",\"data\":[\"p1\"]]</mrljson></oob></template>";
-      
+
       XmlMapper xmlMapper = new XmlMapper();
       Template template = xmlMapper.readValue(xml, Template.class);
-      
+
       System.out.println(template);
-      
-    } catch(Exception e) {
+
+    } catch (Exception e) {
       e.printStackTrace();
     }
-    }
+  }
 
 }
diff --git a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
index 783ec951ff..7b3ee61b19 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/Gateway.java
@@ -83,7 +83,7 @@ default Message getDescribeMsg(String connId) {
             "describe",
             new Object[] {
                 FILL_UUID_MAGIC_VAL,
-                new DescribeQuery(Platform.getLocalInstance().getId(), connId)
+                new DescribeQuery(Runtime.getInstance().getId(), connId)
             }
             );
   }
diff --git a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
new file mode 100644
index 0000000000..5d15601b58
--- /dev/null
+++ b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
@@ -0,0 +1,43 @@
+package org.myrobotlab.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.service.Runtime;
+
+public class ConfigUtilsTest {
+
+  @Before
+  public void beforeTest() {
+    Runtime.releaseAll(true, true);
+  }
+
+  @Test
+  public void testGetResourceRoot() {
+    String resource = ConfigUtils.getResourceRoot();
+    // could be affected by dirty filesystem
+    assertEquals("resource", resource);
+  }
+
+  @Test
+  public void testLoadRuntimeConfig() {
+    String resource = ConfigUtils.getResourceRoot();
+    assertNotNull(resource);
+  }
+
+  @Test
+  public void testLoadStartYml() {
+    StartYml start = ConfigUtils.loadStartYml();
+    assertNotNull(start);
+  }
+
+  @Test
+  public void testGetId() {
+    assertEquals(ConfigUtils.getId(), ConfigUtils.loadRuntimeConfig(null).id);
+  }
+
+
+}
diff --git a/src/test/java/org/myrobotlab/service/OpenCVTest.java b/src/test/java/org/myrobotlab/service/OpenCVTest.java
index fa9e250ae5..137a12a499 100644
--- a/src/test/java/org/myrobotlab/service/OpenCVTest.java
+++ b/src/test/java/org/myrobotlab/service/OpenCVTest.java
@@ -6,6 +6,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
@@ -87,6 +88,12 @@ public static void main(String[] args) {
 
   @Rule
   public final TestName testName = new TestName();
+  
+  @Before
+  public void beforeTest() {
+    cv.reset();
+  }
+
 
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
@@ -222,8 +229,8 @@ public final void testAllFilterTypes() {
 
     for (String fn : OpenCV.POSSIBLE_FILTERS) {
       log.warn("trying filter {}", fn);
-      if (fn.startsWith("DL4J") || fn.startsWith("FaceTraining") || fn.startsWith("Tesseract") || fn.startsWith("SimpleBlobDetector") || fn.startsWith("Solr") || fn.startsWith("Split")) {
-        log.info("skipping {}", fn);
+      if ( fn.startsWith("FaceDetectDNN") || fn.startsWith("FaceRecognizer") || fn.startsWith("DL4J") || fn.startsWith("FaceTraining") || fn.startsWith("Tesseract") || fn.startsWith("SimpleBlobDetector") || fn.startsWith("Solr") || fn.startsWith("Split")) {
+        log.warn("skipping {}", fn);
         continue;
       }
       cv.addFilter(fn);

From 69013e59c5ad3873065b8063e8059c0d6d2a0f89 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 12 Feb 2024 06:20:53 -0800
Subject: [PATCH 046/131] framework updates

---
 Jenkinsfile                                   |   24 +-
 README.md                                     |   36 +
 .../org/myrobotlab/framework/CmdOptions.java  |   62 +-
 .../org/myrobotlab/framework/Platform.java    |   51 +-
 .../org/myrobotlab/framework/Service.java     |   24 +-
 .../org/myrobotlab/framework/StartYml.java    |    4 -
 src/main/java/org/myrobotlab/io/FileIO.java   |   36 +-
 .../java/org/myrobotlab/process/Launcher.java |   43 +-
 .../java/org/myrobotlab/service/Runtime.java  | 1534 ++++++++---------
 .../java/org/myrobotlab/service/WebGui.java   |   46 +-
 .../service/config/RuntimeConfig.java         |   35 +-
 .../org/myrobotlab/codec/CodecUtilsTest.java  |   19 -
 .../myrobotlab/framework/CmdOptionsTest.java  |   36 +-
 .../org/myrobotlab/framework/ConfigTest.java  |  132 +-
 .../java/org/myrobotlab/io/FileIOTest.java    |    6 +-
 .../org/myrobotlab/service/RuntimeTest.java   |   17 +-
 .../org/myrobotlab/service/SerialTest.java    |    2 +-
 17 files changed, 990 insertions(+), 1117 deletions(-)

diff --git a/Jenkinsfile b/Jenkinsfile
index 8e7ccc45f2..63a28dcadb 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -64,18 +64,18 @@ pipeline {
         } // stage build
 
    
-      stage('dependencies') {
-         when {
-               expression { params.verify == 'true' }
-         }
-         steps {
-            script {
-                  sh '''
-                     mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
-                  '''
-            }
-         }
-      } // stage dependencies      
+      // stage('dependencies') {
+      //    when {
+      //          expression { params.verify == 'true' }
+      //    }
+      //    steps {
+      //       script {
+      //             sh '''
+      //                mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
+      //             '''
+      //       }
+      //    }
+      // } // stage dependencies      
 
       // --fail-fast
       // -DargLine="-Xmx1024m"
diff --git a/README.md b/README.md
index 69e1433cbf..f22e3b94f8 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,42 @@ type: Runtime
 virtual: false
 ```
 
+# Starting Flowchart
+```mermaid
+flowchart LR
+    CommandLine[CommandLine]
+    Runtime.main([Runtime.main])
+    install{install}
+    shutdown([shutdown])
+    checkForStartYml{start.yml
+    exists?}
+    startYmlEnabled{start.yml
+    enabled?}
+
+    CommandLine --> Runtime.main
+    Runtime.main --> checkForStartYml
+    checkForStartYml --> |yes| loadStartYml[load start.yml]
+    checkForStartYml --> |no| createDefaultStartYml[create default start.yml]
+    createDefaultStartYml --> loadStartYml
+    loadStartYml --> startYmlEnabled
+    startYmlEnabled --> |yes| Runtime.startConfig[config = start.yml config]
+    startYmlEnabled --> |no| default[config = default]
+    Runtime.startConfig --> loadRuntimeConfig[load runtime config]
+    default --> loadRuntimeConfig
+    loadRuntimeConfig --> startRuntime[start runtime]
+    startRuntime --> applyRuntimeConfig[apply runtime config
+    does not process registry]
+    applyRuntimeConfig --> install{install?}
+
+    install -->|yes| loadServiceData[loadServiceData]
+    install -->|no| Runtime.startConf[get runtime.startConfig config]
+
+    loadServiceData --> findUninstalledDependencies[find uninstallled dependencies]
+    findUninstalledDependencies -->installDependencies[install dependencies]
+    installDependencies --> shutdown
+```
+
+
 # Network Distributed Architecture
 
 ## Websockets - Default Response for New Connection
diff --git a/src/main/java/org/myrobotlab/framework/CmdOptions.java b/src/main/java/org/myrobotlab/framework/CmdOptions.java
index f0eb00c0e7..2c357e8db6 100644
--- a/src/main/java/org/myrobotlab/framework/CmdOptions.java
+++ b/src/main/java/org/myrobotlab/framework/CmdOptions.java
@@ -26,9 +26,7 @@
  * </pre>
  */
 @Command(name = "java -jar myrobotlab.jar ")
-public class CmdOptions {  
-
-  public final String DEFAULT_CONNECT = "http://localhost:8888";
+public class CmdOptions {
 
   static boolean contains(List<String> l, String flag) {
     for (String f : l) {
@@ -39,51 +37,28 @@ static boolean contains(List<String> l, String flag) {
     return false;
   }
 
-  // launcher ??
-  @Option(names = { "-a", "--auto-update" }, description = "auto updating - this feature allows mrl instances to be automatically updated when a new version is available")
-  public boolean autoUpdate = false;
-
   // launcher
   @Option(names = { "-c",
-      "--config" }, fallbackValue="default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
+      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
   public String config = null;
 
-  @Option(names = {
-      "--connect" }, arity = "0..*", /*
-                                      * defaultValue = DEFAULT_CONNECT,
-                                      */ fallbackValue = DEFAULT_CONNECT, description = "connects this mrl instance to another mrl instance - default is " + DEFAULT_CONNECT)
-  public String connect = null;
-
   @Option(names = { "-h", "-?", "--help" }, description = "shows help")
   public boolean help = false;
-  
-  @Option(names = { "-r", "--config-root" }, description = "sets configuration root, the root for which all config directories are in")
-  public String configRoot = null;
-
-
-  @Option(names = { "--id" }, description = "process identifier to be mdns or network overlay name for this instance - one is created at random if not assigned")
+  @Option(names = {
+      "--id" }, description = "process identifier to be mdns or network overlay name for this instance - one is created at random if not assigned")
   public String id;
 
   @Option(names = { "-i",
       "--install" }, arity = "0..*", description = "installs all dependencies for all services, --install {serviceType} installs dependencies for a specific service, if no type is specified then all services are installed")
   public String install[];
 
-  @Option(names = { "-I",
-      "--invoke" }, arity = "0..*", description = "invokes a method on a service --invoke {serviceName} {method} {param0} {param1} ... : --invoke python execFile myFile.py")
-  public String invoke[];
-
-  // for launcher
   @Option(names = { "-j", "--jvm" }, arity = "0..*", description = "jvm parameters for the instance of mrl")
   public String jvm;
 
-  @Option(names = { "-l", "--log-level" }, description = "log level - helpful for troubleshooting [debug info warn error]")
+  @Option(names = { "-l",
+      "--log-level" }, description = "log level - helpful for troubleshooting [debug info warn error]")
   public String logLevel = "info";
 
-  @Option(names = { "--log-file" }, description = "log file name [myrobotlab.log]")
-  public String logFile = "myrobotlab.log";
-
-  // FIXME - highlight or italics for examples !!
-  // launcher
   @Option(names = { "-m", "--memory" }, description = "adjust memory can e.g. -m 2g \n -m 128m")
   public String memory = null;
 
@@ -91,9 +66,6 @@ static boolean contains(List<String> l, String flag) {
       "--services" }, arity = "0..*", description = "services requested on startup, the services must be {name} {Type} paired, e.g. gui SwingGui webgui WebGui servo Servo ...")
   public List<String> services = new ArrayList<>();
 
-  @Option(names = { "-V", "--virtual" }, description = "sets global environment as virtual - all services which support virtual hardware will create virtual hardware")
-  public boolean virtual = false;
-
   public CmdOptions() {
   }
 
@@ -133,34 +105,18 @@ public static String toString(String[] cmdLine) {
    * 
    * @return the list of output command
    * @throws IOException
-   *           boom
+   *                     boom
    * 
    */
   public List<String> getOutputCmd() throws IOException {
 
     List<String> cmd = new ArrayList<>();
 
-    if (autoUpdate) {
-      cmd.add("-a");
-    }
-
     if (config != null) {
       cmd.add("--config");
       cmd.add(config);
     }
 
-    if (connect != null) {
-      cmd.add("-c");
-      cmd.add(connect);
-    }
-
-    if (invoke != null) {
-      cmd.add("-I");
-      for (int i = 0; i < invoke.length; ++i) {
-        cmd.add(invoke[i]);
-      }
-    }
-
     if (help) {
       cmd.add("-h");
     }
@@ -206,10 +162,6 @@ public List<String> getOutputCmd() throws IOException {
       cmd.add(s);
     }
 
-    if (virtual) {
-      cmd.add("-v");
-    }
-
     return cmd;
   }
 
diff --git a/src/main/java/org/myrobotlab/framework/Platform.java b/src/main/java/org/myrobotlab/framework/Platform.java
index 1b1ed4f2d5..5742bf365e 100644
--- a/src/main/java/org/myrobotlab/framework/Platform.java
+++ b/src/main/java/org/myrobotlab/framework/Platform.java
@@ -13,6 +13,7 @@
 import java.util.TreeMap;
 import java.util.zip.ZipFile;
 
+import org.myrobotlab.config.ConfigUtils;
 // Do not pull in deps to this class !
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.logging.Level;
@@ -64,13 +65,7 @@ public class Platform implements Serializable {
   String vmName;
   String vmVersion;
   String mrlVersion;
-  boolean isVirtual = false;
 
-  /**
-   * Static identifier to identify the "instance" of myrobotlab - similar to
-   * network ip of a device and used in a similar way
-   */
-  String id;
   String branch;
 
   String pid;
@@ -95,7 +90,7 @@ public class Platform implements Serializable {
    * All data should be accessed through public functions on the local instance.
    * If the local instance is desired. If its from a serialized instance, the
    * "getters" will be retrieving appropriate info for that serialized instance.
-   * 
+   *  
    * @return - return the local instance of the current platform
    */
   public static Platform getLocalInstance() {
@@ -121,7 +116,8 @@ public static Platform getLocalInstance() {
 
       // === ARCH ===
       String arch = System.getProperty("os.arch").toLowerCase();
-      if ("i386".equals(arch) || "i486".equals(arch) || "i586".equals(arch) || "i686".equals(arch) || "amd64".equals(arch) || arch.startsWith("x86")) {
+      if ("i386".equals(arch) || "i486".equals(arch) || "i586".equals(arch) || "i686".equals(arch)
+          || "amd64".equals(arch) || arch.startsWith("x86")) {
         platform.arch = "x86"; // don't care at the moment
       }
 
@@ -159,7 +155,8 @@ public static Platform getLocalInstance() {
         // tries very hard to hide this from running programs
         String procArch = System.getenv("PROCESSOR_ARCHITECTURE");
         String procArchWow64 = System.getenv("PROCESSOR_ARCHITEW6432");
-        platform.osBitness = (procArch != null && procArch.endsWith("64") || procArchWow64 != null && procArchWow64.endsWith("64")) ? 64 : 32;
+        platform.osBitness = (procArch != null && procArch.endsWith("64")
+            || procArchWow64 != null && procArchWow64.endsWith("64")) ? 64 : 32;
         switch (arch) {
           case "x86":
           case "i386":
@@ -460,19 +457,6 @@ public String toString() {
     return String.format("%s.%d.%s", arch, jvmBitness, os);
   }
 
-  /**
-   * @return The instance identifier of the current running myrobotlab. Used for
-   *         connecting multiple myrobotlabs together
-   * 
-   */
-  public String getId() {
-    // null ids are not allowed
-    if (id == null) {
-      id = NameGenerator.getName();
-    }
-    return id;
-  }
-
   /**
    * @return The Computer's hostname
    */
@@ -480,15 +464,6 @@ public String getHostname() {
     return hostname;
   }
 
-  /**
-   * @param newId
-   *          Set your own instance identifier
-   * 
-   */
-  public void setId(String newId) {
-    id = newId;
-  }
-
   /**
    * @return the time when this instance was started
    * 
@@ -497,20 +472,6 @@ public Date getStartTime() {
     return startTime;
   }
 
-  /**
-   * @return true if running in virtual mode
-   * 
-   */
-  public static boolean isVirtual() {
-    Platform p = getLocalInstance();
-    return p.isVirtual;
-  }
-
-  public static void setVirtual(boolean b) {
-    Platform p = getLocalInstance();
-    p.isVirtual = b;
-  }
-
   public static void main(String[] args) {
     try {
       LoggingFactory.init(Level.DEBUG);
diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 8f0d4ad2bf..3f7acbf755 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -57,6 +57,7 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.myrobotlab.codec.CodecUtils;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.framework.interfaces.Broadcaster;
 import org.myrobotlab.framework.interfaces.ConfigurableService;
@@ -474,22 +475,15 @@ static public String getResourceDir(Class<?> clazz, String additionalPath) {
    *         then it needs an instance of Runtime which is not available.
    * 
    */
-  @Deprecated /* this should not be static - remove it */
   static public String getResourceDir(String serviceType, String additionalPath) {
 
     // setting resource directory
-    String resourceDir = null;
+    String resource = ConfigUtils.getResourceRoot() + fs + serviceType;
 
-    // stupid solution to get past static problem
-    if (!"Runtime".equals(serviceType)) {
-      resourceDir = Runtime.getInstance().getConfig().resource + fs + serviceType;
-    } else {
-      resourceDir = "resource";
-    }
     if (additionalPath != null) {
-      resourceDir = FileIO.gluePaths(resourceDir, additionalPath);
+      resource = FileIO.gluePaths(resource, additionalPath);
     }
-    return resourceDir;
+    return resource;
   }
 
   /**
@@ -516,7 +510,7 @@ public String getResourcePath(String additionalPath) {
    */
 
   static public String getResourceRoot() {
-    return Runtime.getInstance().getConfig().resource;
+    return ConfigUtils.getResourceRoot();//Runtime.getInstance().getConfig().resource;
   }
 
   /**
@@ -625,7 +619,7 @@ public Service(String reservedKey, String inId) {
 
     // necessary for serialized transport\
     if (inId == null) {
-      id = Platform.getLocalInstance().getId();
+      id = ConfigUtils.getId();
       log.debug("creating local service for id {}", id);
     } else {
       id = inId;
@@ -676,7 +670,7 @@ public Service(String reservedKey, String inId) {
     // register this service if local - if we are a foreign service, we probably
     // are being created in a
     // registration already
-    if (id.equals(Platform.getLocalInstance().getId())) {
+    if (id.equals(ConfigUtils.getId())) {
       Registration registration = new Registration(this);
       Runtime.register(registration);
     }
@@ -1510,7 +1504,7 @@ public ServiceConfig getFilteredConfig() {
           // The StringUtils.removeEnd() call is a no-op when the ID is not our
           // local ID,
           // so doesn't conflict with remote routes
-          Listener newConfigListener = new Listener(listener.topicMethod, StringUtil.removeEnd(listener.callbackName, '@' + Platform.getLocalInstance().getId()),
+          Listener newConfigListener = new Listener(listener.topicMethod, StringUtil.removeEnd(listener.callbackName, '@' + Runtime.getInstance().getId()),
               listener.callbackMethod);
           newListeners.add(newConfigListener);
         }
@@ -1614,7 +1608,7 @@ public Service<T> publishState() {
   @Override
   synchronized public void releaseService() {
     // auto release children and unregister
-    Runtime.releaseService(getName());
+    Runtime.releaseServiceInternal(getName());
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/framework/StartYml.java b/src/main/java/org/myrobotlab/framework/StartYml.java
index c8bfb25a44..b1806d203c 100644
--- a/src/main/java/org/myrobotlab/framework/StartYml.java
+++ b/src/main/java/org/myrobotlab/framework/StartYml.java
@@ -9,10 +9,6 @@
  *
  */
 public class StartYml {
-  /**
-   * instance id of myrobotlab, default will be dynamically generated
-   */
-  public String id;  
     
   /**
    * configuration set to start under /data/config/{configName}
diff --git a/src/main/java/org/myrobotlab/io/FileIO.java b/src/main/java/org/myrobotlab/io/FileIO.java
index 2cdad66af2..8b3fe189c4 100644
--- a/src/main/java/org/myrobotlab/io/FileIO.java
+++ b/src/main/java/org/myrobotlab/io/FileIO.java
@@ -58,8 +58,8 @@
 import java.util.zip.ZipException;
 
 import org.apache.commons.io.Charsets;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.Platform;
-import org.myrobotlab.framework.Service;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
@@ -854,8 +854,6 @@ public static void main(String[] args) throws ZipException, IOException {
       f = new File(uri);
       log.info("{} exists {}", uri, f.exists());
 
-      log.info("isJar : {}", isJar());
-
     } catch (Exception e) {
       Logging.logError(e);
     }
@@ -870,33 +868,22 @@ public static void main(String[] args) throws ZipException, IOException {
    *          Python/examples/someFile.py
    * @return byte array
    */
-  @Deprecated /* user Service.getResource(src) */
   static public final byte[] resourceToByteArray(String src) {
 
-    // this path assumes in a jar ?
-    // String filename = "/resource/" + src;
-    log.info("looking for Resource {}", src);
+    log.info("looking for resource {}", src);
     InputStream isr = null;
-    if (isJar()) {
-      // this path assumes in a jar ? ensure it's forward slashes
-      String filename = "/resource/" + src.replace("\\", "/");
-      isr = FileIO.class.getResourceAsStream(filename);
-    } else {
-      String localFilename = Service.getResourceRoot() + File.separator + src;
-      try {
-        isr = new FileInputStream(localFilename);
-      } catch (Exception e) {
-        Logging.logError(e);
-        log.error("File not found. {}", localFilename, e);
-        return null;
-      }
+    String resource = ConfigUtils.getResourceRoot();
+    String localFilename = resource + File.separator + src;
+    try {
+      isr = new FileInputStream(localFilename);
+    } catch (Exception e) {
+      Logging.logError(e);
+      log.error("file not found. {}", localFilename, e);
+      return null;
     }
+
     byte[] data = null;
     try {
-      if (isr == null) {
-        log.error("can not find resource [{}]", src);
-        return null;
-      }
       data = toByteArray(isr);
     } finally {
       try {
@@ -918,7 +905,6 @@ static public final byte[] resourceToByteArray(String src) {
    *          Python/examples/someFile.py
    * @return string
    */
-  @Deprecated /* use Service.getResourceAsString(src) */
   static public final String resourceToString(String src) {
     byte[] bytes = resourceToByteArray(src);
     if (bytes == null) {
diff --git a/src/main/java/org/myrobotlab/process/Launcher.java b/src/main/java/org/myrobotlab/process/Launcher.java
index 3e819f3c59..7954fd7d5d 100644
--- a/src/main/java/org/myrobotlab/process/Launcher.java
+++ b/src/main/java/org/myrobotlab/process/Launcher.java
@@ -2,9 +2,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -241,41 +238,15 @@ public static void main(String[] args) {
         return;
       }
 
-      boolean instanceAlreadyRunning = false;
-
-      try {
-        URI uri = new URI(options.connect);
-        Socket socket = new Socket();
-        socket.connect(new InetSocketAddress(uri.getHost(), uri.getPort()), 1000);
-        socket.close();
-        instanceAlreadyRunning = true;
-      } catch (Exception e) {
-        log.info("could not connect to {}", options.connect);
+      log.info("spawning new instance");
+      ProcessBuilder builder = createBuilder(options);
+      process = builder.start();
+      if (process.isAlive()) {
+        log.info("process is alive");
+      } else {
+        log.error("process died");
       }
 
-      if (instanceAlreadyRunning && options.connect.equals(options.DEFAULT_CONNECT)) {
-        log.error("zombie instance already running at {}", options.DEFAULT_CONNECT);
-        return;
-      }
-
-      if (!instanceAlreadyRunning || !options.connect.equals(options.DEFAULT_CONNECT)) {
-        log.info("spawning new instance");
-        ProcessBuilder builder = createBuilder(options);
-        process = builder.start();
-        if (process.isAlive()) {
-          log.info("process is alive");
-        } else {
-          log.error("process died");
-        }
-      }
-
-      /*
-       * // FIXME - use wsclient for remote access if (options.client != null) {
-       * // FIXME - delay & auto connect Client.main(new String[] { "-c",
-       * options.client }); } else { // terminating - "if" runtime exists - if
-       * not no biggy Runtime.shutdown(); }
-       */
-
     } catch (Exception e) {
       log.error("main threw", e);
     }
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index e5da31305f..e838cf6c52 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -47,6 +47,7 @@
 import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.codec.CodecUtils.ApiDescription;
 import org.myrobotlab.codec.ForeignProcessUtils;
+import org.myrobotlab.config.ConfigUtils;
 import org.myrobotlab.framework.CmdOptions;
 import org.myrobotlab.framework.DescribeQuery;
 import org.myrobotlab.framework.DescribeResults;
@@ -129,7 +130,7 @@
  *
  */
 public class Runtime extends Service<RuntimeConfig> implements MessageListener, ServiceLifeCyclePublisher, RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider {
-  
+
   final static private long serialVersionUID = 1L;
 
   // FIXME - AVOID STATIC FIELDS !!! use .getInstance() to get the singleton
@@ -167,6 +168,8 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
 
   protected final Map<String, Set<String>> typeToInterface = new HashMap<>();
 
+  private transient static final Object processLock = new Object();
+
   /**
    * FILTERED_INTERFACES are the set of low level interfaces which we are
    * interested in filtering out if we want to maintain a data structure which
@@ -188,7 +191,13 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
    * name. It cannot be null, it cannot have "/" or "\" in the name - it has to
    * be a valid file name for the OS. It's defaulted to "default". Changed often
    */
-  protected String configName = "default";
+  protected static String configName = "default";
+
+  /**
+   * The runtime config which Runtime was started with. This is the config which
+   * will be applied to Runtime when its created on startup.
+   */
+  // protected static RuntimeConfig startConfig = null;
 
   /**
    * State variable reporting if runtime is currently starting services from
@@ -378,30 +387,32 @@ static public ServiceInterface create(String name) {
    *          - Can be null if a service file exists for named service
    * @return the service
    */
-  static public synchronized ServiceInterface create(String name, String type) {
+  static public ServiceInterface create(String name, String type) {
 
-    try {
-      ServiceInterface si = Runtime.getService(name);
-      if (si != null) {
-        return si;
-      }
+    synchronized (processLock) {
 
-      // FIXME remove configName from loadService
-      Plan plan = Runtime.load(name, type);
-      Runtime.check(name, type);
-      // at this point - the plan should be loaded, now its time to create the
-      // children peers
-      // and parent service
-      createServicesFromPlan(plan, null, name);
-      si = Runtime.getService(name);
-      if (si == null) {
-        Runtime.getInstance().error("coult not create %s of type %s", name, type);
+      try {
+        ServiceInterface si = Runtime.getService(name);
+        if (si != null) {
+          return si;
+        }
+
+        Plan plan = Runtime.load(name, type);
+        Runtime.check(name, type);
+        // at this point - the plan should be loaded, now its time to create the
+        // children peers
+        // and parent service
+        createServicesFromPlan(plan, null, name);
+        si = Runtime.getService(name);
+        if (si == null) {
+          Runtime.getInstance().error("coult not create %s of type %s", name, type);
+        }
+        return si;
+      } catch (Exception e) {
+        runtime.error(e);
       }
-      return si;
-    } catch (Exception e) {
-      runtime.error(e);
+      return null;
     }
-    return null;
   }
 
   /**
@@ -414,43 +425,46 @@ static public synchronized ServiceInterface create(String name, String type) {
    * @param name
    * @return
    */
-  synchronized private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, Map<String, ServiceInterface> createdServices, String name) {
+  private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, Map<String, ServiceInterface> createdServices, String name) {
 
-    if (createdServices == null) {
-      createdServices = new LinkedHashMap<>();
-    }
-
-    // Plan's config
-    RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
-    // current Runtime config
-    RuntimeConfig currentConfig = Runtime.getInstance().config;
+    synchronized (processLock) {
 
-    for (String service : plansRtConfig.getRegistry()) {
-      // FIXME - determine if you want to return a complete merge of activated
-      // or just "recent"
-      if (Runtime.getService(service) != null) {
-        continue;
+      if (createdServices == null) {
+        createdServices = new LinkedHashMap<>();
       }
-      ServiceConfig sc = plan.get(service);
-      if (sc == null) {
-        runtime.error("could not get %s from plan", service);
-        continue;
-      }
-      ServiceInterface si = createService(service, sc.type, null);
-      // process the base listeners/subscription of ServiceConfig
-      si.addConfigListeners(sc);
-      if (si instanceof ConfigurableService) {
-        try {
-          ((ConfigurableService) si).apply(sc);
-        } catch (Exception e) {
-          Runtime.getInstance().error("could not apply config of type %s to service %s, using default config", sc.type, si.getName(), sc.type);
+
+      // Plan's config
+      RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
+      // current Runtime config
+      RuntimeConfig currentConfig = Runtime.getInstance().config;
+
+      for (String service : plansRtConfig.getRegistry()) {
+        // FIXME - determine if you want to return a complete merge of activated
+        // or just "recent"
+        if (Runtime.getService(service) != null) {
+          continue;
+        }
+        ServiceConfig sc = plan.get(service);
+        if (sc == null) {
+          runtime.error("could not get %s from plan", service);
+          continue;
         }
+        ServiceInterface si = createService(service, sc.type, null);
+        // process the base listeners/subscription of ServiceConfig
+        si.addConfigListeners(sc);
+        if (si instanceof ConfigurableService) {
+          try {
+            ((ConfigurableService) si).apply(sc);
+          } catch (Exception e) {
+            Runtime.getInstance().error("could not apply config of type %s to service %s, using default config", sc.type, si.getName(), sc.type);
+          }
+        }
+        createdServices.put(service, si);
+        currentConfig.add(service);
       }
-      createdServices.put(service, si);
-      currentConfig.add(service);
-    }
 
-    return createdServices;
+      return createdServices;
+    }
   }
 
   public String getServiceExample(String serviceType) {
@@ -582,7 +596,9 @@ public final static void createAndStartServices(List<String> services) {
    */
   @Override
   public boolean setVirtual(boolean b) {
-    boolean changed = isVirtual != b;
+    boolean changed = config.virtual != b;
+    config.virtual = b;
+    isVirtual = b;
     setAllVirtual(b);
     if (changed) {
       broadcastState();
@@ -599,13 +615,12 @@ public boolean setVirtual(boolean b) {
    * @return b
    */
   static public boolean setAllVirtual(boolean b) {
-    Platform.setVirtual(b);
     for (ServiceInterface si : getServices()) {
       if (!si.isRuntime()) {
         si.setVirtual(b);
       }
     }
-    Runtime.getInstance().isVirtual = b;
+    Runtime.getInstance().config.virtual = b;
     Runtime.getInstance().broadcastState();
     return b;
   }
@@ -622,7 +637,6 @@ static public boolean setAllVirtual(boolean b) {
    */
   public void setAutoStart(boolean autoStart) throws IOException {
     log.debug("setAutoStart {}", autoStart);
-    startYml.id = getId();
     startYml.enable = autoStart;
     startYml.config = configName;
     FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
@@ -657,126 +671,128 @@ public void setAutoStart(boolean autoStart) throws IOException {
    *         '/', or a service with the same name exists but has a different
    *         type, will return null instead.
    */
-  static private synchronized ServiceInterface createService(String name, String type, String inId) {
-    log.info("Runtime.createService {}", name);
+  static private ServiceInterface createService(String name, String type, String inId) {
+    synchronized (processLock) {
+      log.info("Runtime.createService {}", name);
 
-    if (name == null) {
-      runtime.error("service name cannot be null");
+      if (name == null) {
+        runtime.error("service name cannot be null");
 
-      return null;
-    }
+        return null;
+      }
 
-    if (name.contains("@") || name.contains("/")) {
-      runtime.error("service name cannot contain '@' or '/': {}", name);
+      if (name.contains("@") || name.contains("/")) {
+        runtime.error("service name cannot contain '@' or '/': {}", name);
 
-      return null;
-    }
+        return null;
+      }
 
-    String fullName;
-    if (inId == null || inId.equals(""))
-      fullName = getFullName(name);
-    else
-      fullName = String.format("%s@%s", name, inId);
+      String fullName;
+      if (inId == null || inId.equals(""))
+        fullName = getFullName(name);
+      else
+        fullName = String.format("%s@%s", name, inId);
 
-    if (type == null) {
-      ServiceConfig sc;
-      try {
-        sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
-      } catch (IOException e) {
-        runtime.error("could not find type for service %s", name);
-        return null;
+      if (type == null) {
+        ServiceConfig sc;
+        try {
+          sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
+        } catch (IOException e) {
+          runtime.error("could not find type for service %s", name);
+          return null;
+        }
+        if (sc != null) {
+          log.info("found type for {} in plan", name);
+          type = sc.type;
+        } else {
+          runtime.error("createService type not specified and could not get type for {} from plan", name);
+          return null;
+        }
       }
-      if (sc != null) {
-        log.info("found type for {} in plan", name);
-        type = sc.type;
-      } else {
-        runtime.error("createService type not specified and could not get type for {} from plan", name);
+
+      if (type == null) {
+        runtime.error("cannot create service {} no type in plan or yml file", name);
         return null;
       }
-    }
 
-    if (type == null) {
-      runtime.error("cannot create service {} no type in plan or yml file", name);
-      return null;
-    }
+      String fullTypeName = CodecUtils.makeFullTypeName(type);
+
+      ServiceInterface si = Runtime.getService(fullName);
+      if (si != null) {
+        if (!si.getTypeKey().equals(fullTypeName)) {
+          runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type);
+          return null;
+        }
+        return si;
+      }
 
-    String fullTypeName = CodecUtils.makeFullTypeName(type);
+      // DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
+      // only try to resolve type by the plan - if not then error out
 
-    ServiceInterface si = Runtime.getService(fullName);
-    if (si != null) {
-      if (!si.getTypeKey().equals(fullTypeName)) {
-        runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type);
+      String id = (inId == null) ? Runtime.getInstance().getId() : inId;
+      if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
+        log.error("{} not a type or {} not defined ", fullTypeName, name);
         return null;
       }
-      return si;
-    }
 
-    // DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
-    // only try to resolve type by the plan - if not then error out
-
-    String id = (inId == null) ? Platform.getLocalInstance().getId() : inId;
-    if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
-      log.error("{} not a type or {} not defined ", fullTypeName, name);
-      return null;
-    }
+      // TODO - test new create of existing service
+      ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
+      if (sw != null) {
+        log.info("service {} already exists", name);
+        return sw;
+      }
 
-    // TODO - test new create of existing service
-    ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
-    if (sw != null) {
-      log.info("service {} already exists", name);
-      return sw;
-    }
+      try {
 
-    try {
+        if (log.isDebugEnabled()) {
+          // TODO - determine if there have been new classes added from
+          // ivy --> Boot Classloader --> Ext ClassLoader --> System
+          // ClassLoader
+          // http://blog.jamesdbloom.com/JVMInternals.html
+          log.debug("ABOUT TO LOAD CLASS");
+          log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
+          log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("system class loader " + ClassLoader.getSystemClassLoader());
+          log.debug("parent should be null" + ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("thread context " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
+          log.debug("thread context parent " + Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
+        }
 
-      if (log.isDebugEnabled()) {
-        // TODO - determine if there have been new classes added from
-        // ivy --> Boot Classloader --> Ext ClassLoader --> System
-        // ClassLoader
-        // http://blog.jamesdbloom.com/JVMInternals.html
-        log.debug("ABOUT TO LOAD CLASS");
-        log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
-        log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
-        log.debug("system class loader " + ClassLoader.getSystemClassLoader());
-        log.debug("parent should be null" + ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
-        log.debug("thread context " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
-        log.debug("thread context parent " + Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
-      }
+        // FIXME - error if deps are missing - prompt license
+        // require restart !
+        // FIXME - this should happen after inspecting the "loaded" "plan" not
+        // during the create/start/apply !
+
+        // create an instance
+        Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
+        log.debug("returning {}", fullTypeName);
+        si = (ServiceInterface) newService;
+
+        // si.setId(id);
+        if (Runtime.getInstance().getId().equals(id)) {
+          si.setVirtual(Runtime.getInstance().isVirtual());
+          Runtime.getInstance().creationCount++;
+          si.setOrder(Runtime.getInstance().creationCount);
+        }
 
-      // FIXME - error if deps are missing - prompt license
-      // require restart !
-      // FIXME - this should happen after inspecting the "loaded" "plan" not
-      // during the create/start/apply !
-
-      // create an instance
-      Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
-      log.debug("returning {}", fullTypeName);
-      si = (ServiceInterface) newService;
-
-      // si.setId(id);
-      if (Platform.getLocalInstance().getId().equals(id)) {
-        si.setVirtual(Platform.isVirtual());
-        Runtime.getInstance().creationCount++;
-        si.setOrder(Runtime.getInstance().creationCount);
-      }
+        if (runtime != null) {
 
-      if (runtime != null) {
+          runtime.invoke("created", getFullName(name));
 
-        runtime.invoke("created", getFullName(name));
+          // add all the service life cycle subscriptions
+          // runtime.addListener("registered", name);
+          // runtime.addListener("created", name);
+          // runtime.addListener("started", name);
+          // runtime.addListener("stopped", name);
+          // runtime.addListener("released", name);
+        }
 
-        // add all the service life cycle subscriptions
-        // runtime.addListener("registered", name);
-        // runtime.addListener("created", name);
-        // runtime.addListener("started", name);
-        // runtime.addListener("stopped", name);
-        // runtime.addListener("released", name);
+        return (Service) newService;
+      } catch (Exception e) {
+        log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
       }
-
-      return (Service) newService;
-    } catch (Exception e) {
-      log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
+      return null;
     }
-    return null;
   }
 
   static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries() {
@@ -883,58 +899,48 @@ public static final long getFreeMemory() {
   public static Runtime getInstance() {
     if (runtime == null) {
       synchronized (INSTANCE_LOCK) {
-        if (runtime == null) {
+        try {
 
-          // all though this is appropriate it cannot be done
-          // because you need runtime to correctly load/start/etc the plan
-          // so it needs to be bootstrapped
-          // load("runtime", "Runtime");
+          RuntimeConfig c = null;
+          if (runtime == null) {
+            c = ConfigUtils.loadRuntimeConfig(options);
 
-          // just create Runtime
-          runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", Platform.getLocalInstance().getId());
-        }
-        try {
-          // a bit backwards - it loads after it been created
-          // but its necessary because you need an runtime instance before you
-          // load
-
-          File cfgRoot = new File(ROOT_CONFIG_DIR);
-          cfgRoot.mkdirs();
-          if (startYml.enable) {
-            Runtime.load("runtime", "Runtime");
-          }
-          runtime.config.add("runtime");
+            runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", c.id);
+            runtime.startService();
+            // klunky
+            Runtime.register(new Registration(runtime));
 
-          runtime.startService();
-          // platform virtual is higher priority than service virtual
-          Runtime.setAllVirtual(Platform.isVirtual());
+            // assign, do not apply otherwise there will be
+            // a chicken-egg problem
+            runtime.config = c;
+          }
 
-          // setting the singleton security
-          Security.getInstance();
           runtime.getRepo().addStatusPublisher(runtime);
+          runtime.startService();
+          // extract resources "if a jar"
           FileIO.extractResources();
-          // protected services we don't want to remove when releasing a config
-          runtime.startingServices.add("runtime");
-          runtime.startingServices.add("security");
-          runtime.startingServices.add("webgui");
-          runtime.startingServices.add("python");
-
           runtime.startInteractiveMode();
 
-          try {
-            if (options.config != null) {
-              Runtime.startConfig(options.config);
-            } else if (startYml != null && startYml.config != null && startYml.enable) {
+          if (Runtime.options.install != null) {
+            // minimal processed runtime - return it
+            return runtime;
+          }
+
+          runtime.apply(c);
+
+          if (options.services != null) {
+            log.info("command line override for services created");
+            createAndStartServices(options.services);
+          } else {
+            log.info("processing config.registry");
+            if (startYml.enable) {
               Runtime.startConfig(startYml.config);
             }
-          } catch (Exception e) {
-            log.info("runtime will not be loading config");
           }
 
         } catch (Exception e) {
-          log.error("runtime will not be loading config", e);
+          log.error("runtime getInstance threw", e);
         }
-
       } // synchronized lock
     }
 
@@ -1099,7 +1105,7 @@ public static Map<String, ServiceInterface> getLocalServices() {
     Map<String, ServiceInterface> local = new HashMap<>();
     for (String serviceName : registry.keySet()) {
       // FIXME @ should be a requirement of "all" entries for consistency
-      if (!serviceName.contains("@") || serviceName.endsWith(String.format("@%s", Platform.getLocalInstance().getId()))) {
+      if (!serviceName.contains("@") || serviceName.endsWith(String.format("@%s", Runtime.getInstance().getId()))) {
         local.put(serviceName, registry.get(serviceName));
       }
     }
@@ -1149,8 +1155,10 @@ public static Map<String, MethodEntry> getMethodMap(String inName) {
    *
    * @return list of registrations
    */
-  synchronized public List<Registration> getServiceList() {
-    return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList());
+  public List<Registration> getServiceList() {
+    synchronized (processLock) {
+      return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList());
+    }
   }
 
   // FIXME - scary function - returns private data
@@ -1196,10 +1204,16 @@ public static <S extends ServiceInterface> S getService(String inName, StaticTyp
    *
    */
   static public String[] getServiceNames() {
-    Set<String> ret = registry.keySet();
+    Set<String> ret = registry.keySet();    
     String[] services = new String[ret.size()];
-
-    String localId = Platform.getLocalInstance().getId();
+    if (ret.size() == 0) {
+      return services;
+    }
+    
+    // if there are more than 0 services we need runtime
+    // to filter to make sure they are "local"
+    // and this requires a runtime service
+    String localId = Runtime.getInstance().getId();
     int cnt = 0;
     for (String fullname : ret) {
       if (fullname.endsWith(String.format("@%s", localId))) {
@@ -1344,22 +1358,24 @@ public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze)
                * no longer used or needed - change events are pushed no longer
                * pulled <-- Over complicated solution
                */
-  public static synchronized List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
-    List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
-
-    for (String service : getServiceNames()) {
-      Class<?> clazz = getService(service).getClass();
-      while (clazz != null) {
-        for (Class<?> inter : clazz.getInterfaces()) {
-          if (inter.getName().equals(interfaze.getName())) {
-            ret.add(getService(service));
-            continue;
+  public static List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
+    synchronized (processLock) {
+      List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
+
+      for (String service : getServiceNames()) {
+        Class<?> clazz = getService(service).getClass();
+        while (clazz != null) {
+          for (Class<?> inter : clazz.getInterfaces()) {
+            if (inter.getName().equals(interfaze.getName())) {
+              ret.add(getService(service));
+              continue;
+            }
           }
+          clazz = clazz.getSuperclass();
         }
-        clazz = clazz.getSuperclass();
       }
+      return ret;
     }
-    return ret;
   }
 
   /**
@@ -1539,35 +1555,36 @@ static public void install(String serviceType) {
    *          if this should block until done.
    *
    */
-  synchronized static public void install(String serviceType, Boolean blocking) {
-    Runtime r = getInstance();
+  static public void install(String serviceType, Boolean blocking) {
+    synchronized (processLock) {
+      Runtime r = getInstance();
 
-    if (blocking == null) {
-      blocking = false;
-    }
+      if (blocking == null) {
+        blocking = false;
+      }
 
-    installerThread = new Thread() {
-      @Override
-      public void run() {
-        try {
-          if (serviceType == null) {
-            r.getRepo().install();
-          } else {
-            r.getRepo().install(serviceType);
+      installerThread = new Thread() {
+        @Override
+        public void run() {
+          try {
+            if (serviceType == null) {
+              r.getRepo().install();
+            } else {
+              r.getRepo().install(serviceType);
+            }
+          } catch (Exception e) {
+            r.error("dependencies failed - install error", e);
+            throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
           }
-        } catch (Exception e) {
-          r.error("dependencies failed - install error", e);
-          throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
         }
-      }
-    };
+      };
 
-    if (blocking) {
-      installerThread.run();
-    } else {
-      installerThread.start();
+      if (blocking) {
+        installerThread.run();
+      } else {
+        installerThread.start();
+      }
     }
-
   }
 
   /**
@@ -1608,7 +1625,7 @@ static public void invokeCommands(String[] invoke) {
    */
   public static boolean isLocal(String serviceName) {
     ServiceInterface sw = getService(serviceName);
-    return Objects.equals(sw.getId(), Platform.getLocalInstance().getId());
+    return Objects.equals(sw.getId(), Runtime.getInstance().getId());
   }
 
   /*
@@ -1705,10 +1722,12 @@ public void onState(ServiceInterface updatedService) {
     registry.put(String.format("%s@%s", updatedService.getName(), updatedService.getId()), updatedService);
   }
 
-  public static synchronized Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
-    Registration proxy = new Registration(id, name, typeKey, interfaces);
-    register(proxy);
-    return proxy;
+  public static Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
+    synchronized (processLock) {
+      Registration proxy = new Registration(id, name, typeKey, interfaces);
+      register(proxy);
+      return proxy;
+    }
   }
 
   /**
@@ -1732,167 +1751,174 @@ public static synchronized Registration register(String id, String name, String
    * @return registration
    *
    */
-  public static synchronized Registration register(Registration registration) {
+  public static Registration register(Registration registration) {
+    synchronized (processLock) {
+      try {
 
-    try {
+        // TODO - have rules on what registrations to accept - dependent on
+        // security, desire, re-broadcasting configuration etc.
 
-      // TODO - have rules on what registrations to accept - dependent on
-      // security, desire, re-broadcasting configuration etc.
+        String fullname = String.format("%s@%s", registration.getName(), registration.getId());
+        if (registry.containsKey(fullname)) {
+          log.info("{} already registered", fullname);
+          return registration;
+        }
 
-      String fullname = String.format("%s@%s", registration.getName(), registration.getId());
-      if (registry.containsKey(fullname)) {
-        log.info("{} already registered", fullname);
-        return registration;
-      }
+        // if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
+        // log.error("Invalid type key being registered: " +
+        // registration.getTypeKey());
+        // return null;
+        // }
 
-      // if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
-      // log.error("Invalid type key being registered: " +
-      // registration.getTypeKey());
-      // return null;
-      // }
+        log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), ConfigUtils.getId(), registration.getTypeKey());
 
-      log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), Platform.getLocalInstance().getId(), registration.getTypeKey());
+        if (!registration.isLocal(ConfigUtils.getId())) {
 
-      if (!registration.isLocal(Platform.getLocalInstance().getId())) {
+          // Check if we're registering a java service
+          if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
 
-        // Check if we're registering a java service
-        if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
+            String fullTypeName;
+            if (registration.getTypeKey().contains(".")) {
+              fullTypeName = registration.getTypeKey();
+            } else {
+              fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
+            }
 
-          String fullTypeName;
-          if (registration.getTypeKey().contains(".")) {
-            fullTypeName = registration.getTypeKey();
+            try {
+              // de-serialize, class exists
+              registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId());
+              if (registration.getState() != null) {
+                copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
+              }
+            } catch (ClassNotFoundException classNotFoundException) {
+              log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException);
+              return null;
+            }
           } else {
-            fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
-          }
-
-          try {
-            // de-serialize, class exists
-            registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId());
-            if (registration.getState() != null) {
-              copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
+            // We're registering a foreign process service. We don't need to
+            // check
+            // ForeignProcessUtils.isForeignTypeKey() because the type key is
+            // valid
+            // but is not a java class name
+
+            // Class does not exist, check if registration has empty interfaces
+            // Interfaces should always include ServiceInterface if coming from
+            // remote client
+            if (registration.interfaces == null || registration.interfaces.isEmpty()) {
+              log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey());
+              return null;
             }
-          } catch (ClassNotFoundException classNotFoundException) {
-            log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException);
-            return null;
-          }
-        } else {
-          // We're registering a foreign process service. We don't need to check
-          // ForeignProcessUtils.isForeignTypeKey() because the type key is
-          // valid
-          // but is not a java class name
-
-          // Class does not exist, check if registration has empty interfaces
-          // Interfaces should always include ServiceInterface if coming from
-          // remote client
-          if (registration.interfaces == null || registration.interfaces.isEmpty()) {
-            log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey());
-            return null;
-          }
 
-          // FIXME - probably some more clear definition about the requirements
-          // of remote
-          // service registration
-          // In general, there should be very few requirements if any, besides
-          // providing a
-          // name, and the proxy
-          // interface should be responsible for creating a minimal
-          // interpretation
-          // (ServiceInterface) for the remote
-          // service
-
-          // Class<?>[] interfaces = registration.interfaces.stream().map(i -> {
-          // try {
-          // return Class.forName(i);
-          // } catch (ClassNotFoundException e) {
-          // throw new RuntimeException("Unable to load interface " + i + "
-          // defined in remote registration " + registration, e);
-          // }
-          // }).toArray(Class<?>[]::new);
-
-          // registration.service = (ServiceInterface)
-          // Proxy.newProxyInstance(Runtime.class.getClassLoader(), interfaces,
-          // new ProxyServiceInvocationHandler(registration.getName(),
-          // registration.getId()));
-          try {
-            registration.service = ProxyFactory.createProxyService(registration);
-            log.info("Created proxy: " + registration.service);
-          } catch (Exception e) {
-            // at the moment preventing throw
-            Runtime.getInstance().error(e);
+            // FIXME - probably some more clear definition about the
+            // requirements
+            // of remote
+            // service registration
+            // In general, there should be very few requirements if any, besides
+            // providing a
+            // name, and the proxy
+            // interface should be responsible for creating a minimal
+            // interpretation
+            // (ServiceInterface) for the remote
+            // service
+
+            // Class<?>[] interfaces = registration.interfaces.stream().map(i ->
+            // {
+            // try {
+            // return Class.forName(i);
+            // } catch (ClassNotFoundException e) {
+            // throw new RuntimeException("Unable to load interface " + i + "
+            // defined in remote registration " + registration, e);
+            // }
+            // }).toArray(Class<?>[]::new);
+
+            // registration.service = (ServiceInterface)
+            // Proxy.newProxyInstance(Runtime.class.getClassLoader(),
+            // interfaces,
+            // new ProxyServiceInvocationHandler(registration.getName(),
+            // registration.getId()));
+            try {
+              registration.service = ProxyFactory.createProxyService(registration);
+              log.info("Created proxy: " + registration.service);
+            } catch (Exception e) {
+              // at the moment preventing throw
+              Runtime.getInstance().error(e);
+            }
           }
         }
-      }
-
-      registry.put(fullname, registration.service);
-
-      if (runtime != null) {
-
-        String type = registration.getTypeKey();
 
-        // If type does not exist in typeToNames, make it an empty hash set and
-        // return it
-        Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
-        names.add(fullname);
-
-        // FIXME - most of this could be static as it represents meta data of
-        // class and interfaces
+        registry.put(fullname, registration.service);
+
+        if (runtime != null) {
+
+          String type = registration.getTypeKey();
+
+          // If type does not exist in typeToNames, make it an empty hash set
+          // and
+          // return it
+          Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
+          names.add(fullname);
+
+          // FIXME - most of this could be static as it represents meta data of
+          // class and interfaces
+
+          // FIXME - was false - setting now to true .. because
+          // 1 edge case - "can something fulfill my need of an interface - is
+          // not
+          // currently
+          // switching to true
+          boolean updatedServiceLists = false;
+
+          // maintaining interface type relations
+          // see if this service type is new
+          // PROCESS INDEXES ! - FIXME - will need this in unregister
+          // ALL CLASS/TYPE PROCESSING only needs to happen once per type
+          if (!runtime.serviceTypes.contains(type)) {
+            // CHECK IF "CAN FULFILL"
+            // add the interfaces of the new service type
+            Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES);
+            for (String interfaze : interfaces) {
+              Set<String> types = runtime.interfaceToType.get(interfaze);
+              if (types == null) {
+                types = new HashSet<>();
+              }
+              types.add(registration.getTypeKey());
+              runtime.interfaceToType.put(interfaze, types);
+            }
 
-        // FIXME - was false - setting now to true .. because
-        // 1 edge case - "can something fulfill my need of an interface - is not
-        // currently
-        // switching to true
-        boolean updatedServiceLists = false;
+            runtime.typeToInterface.put(type, interfaces);
+            runtime.serviceTypes.add(registration.getTypeKey());
+            updatedServiceLists = true;
+          }
 
-        // maintaining interface type relations
-        // see if this service type is new
-        // PROCESS INDEXES ! - FIXME - will need this in unregister
-        // ALL CLASS/TYPE PROCESSING only needs to happen once per type
-        if (!runtime.serviceTypes.contains(type)) {
-          // CHECK IF "CAN FULFILL"
-          // add the interfaces of the new service type
-          Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES);
-          for (String interfaze : interfaces) {
-            Set<String> types = runtime.interfaceToType.get(interfaze);
-            if (types == null) {
-              types = new HashSet<>();
+          // check to see if any of our interfaces can fulfill requested ones
+          Set<String> myInterfaces = runtime.typeToInterface.get(type);
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).add(fullname);
+              updatedServiceLists = true;
             }
-            types.add(registration.getTypeKey());
-            runtime.interfaceToType.put(interfaze, types);
           }
 
-          runtime.typeToInterface.put(type, interfaces);
-          runtime.serviceTypes.add(registration.getTypeKey());
-          updatedServiceLists = true;
-        }
-
-        // check to see if any of our interfaces can fulfill requested ones
-        Set<String> myInterfaces = runtime.typeToInterface.get(type);
-        for (String inter : myInterfaces) {
-          if (runtime.interfaceToNames.containsKey(inter)) {
-            runtime.interfaceToNames.get(inter).add(fullname);
-            updatedServiceLists = true;
+          if (updatedServiceLists) {
+            runtime.invoke("publishInterfaceToNames");
           }
-        }
 
-        if (updatedServiceLists) {
-          runtime.invoke("publishInterfaceToNames");
+          // TODO - determine rules on re-broadcasting based on configuration
+          runtime.invoke("registered", registration);
         }
 
-        // TODO - determine rules on re-broadcasting based on configuration
-        runtime.invoke("registered", registration);
-      }
+        // TODO - remove ? already get state from registration
+        if (!registration.isLocal(ConfigUtils.getId())) {
+          runtime.subscribe(registration.getFullName(), "publishState");
+        }
 
-      // TODO - remove ? already get state from registration
-      if (!registration.isLocal(Platform.getLocalInstance().getId())) {
-        runtime.subscribe(registration.getFullName(), "publishState");
+      } catch (Exception e) {
+        log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
+        return null;
       }
 
-    } catch (Exception e) {
-      log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
-      return null;
+      return registration;
     }
-
-    return registration;
   }
 
   /**
@@ -1906,60 +1932,79 @@ public static synchronized Registration register(Registration registration) {
    * @return true/false
    *
    */
-  public synchronized static boolean releaseService(String inName) {
-    if (inName == null) {
-      log.debug("release (null)");
-      return false;
+  public static boolean releaseService(String inName) {
+    ServiceInterface sc = getService(inName);
+    if (sc != null) {
+      sc.releaseService();
+      return true;
     }
+    return false;
+  }
+  
+  
+  /**
+   * Called after any subclassed releaseService has been called, this cleans
+   * up the registry and removes peers
+   * @param inName
+   * @return
+   */
+  public static boolean releaseServiceInternal(String inName) {
+    synchronized (processLock) {
+      if (inName == null) {
+        log.debug("release (null)");
+        return false;
+      }
 
-    String name = getFullName(inName);
+      String name = getFullName(inName);
 
-    String id = CodecUtils.getId(name);
-    if (!id.equals(Platform.getLocalInstance().getId())) {
-      log.warn("will only release local services - %s is remote", name);
-      return false;
-    }
+      String id = CodecUtils.getId(name);
+      if (!id.equals(Runtime.getInstance().getId())) {
+        log.warn("will only release local services - %s is remote", name);
+        return false;
+      }
 
-    log.info("releasing service {}", name);
+      log.info("releasing service {}", name);
 
-    if (!registry.containsKey(name)) {
-      log.info("{} not registered", name);
-      return false;
-    }
+      if (!registry.containsKey(name)) {
+        log.info("{} not registered", name);
+        return false;
+      }
 
-    // get reference from registry
-    ServiceInterface si = registry.get(name);
-    if (si == null) {
-      log.warn("cannot release {} - not in registry");
-      return false;
-    }
+      // get reference from registry
+      ServiceInterface si = registry.get(name);
+      if (si == null) {
+        log.warn("cannot release {} - not in registry");
+        return false;
+      }
 
-    // FIXME - TODO invoke and or blocking on preRelease - Future
+      // FIXME - TODO invoke and or blocking on preRelease - Future
 
-    // send msg to service to self terminate
-    if (si.isLocal()) {
-      si.purgeTasks();
-      si.stopService();
-    } else {
-      if (runtime != null) {
-        runtime.send(name, "releaseService");
+      // send msg to service to self terminate
+      if (si.isLocal()) {
+        si.purgeTasks();
+        si.stopService();
+      } else {
+        if (runtime != null) {
+          runtime.send(name, "releaseService");
+        }
       }
-    }
 
-    // recursive peer release
-    Map<String, Peer> peers = si.getPeers();
-    if (peers != null) {
-      for (Peer peer : peers.values()) {
-        release(peer.name);
+      // recursive peer release
+      Map<String, Peer> peers = si.getPeers();
+      if (peers != null) {
+        for (Peer peer : peers.values()) {
+          release(peer.name);
+        }
       }
-    }
 
-    // FOR remote this isn't correct - it should wait for
-    // a message from the other runtime to say that its released
-    unregister(name);
-    return true;
+      // FOR remote this isn't correct - it should wait for
+      // a message from the other runtime to say that its released
+      unregister(name);
+      return true;
+    }
   }
 
+
   /**
    * Removes registration for a service. Removes the service from
    * {@link #typeToInterface} and {@link #interfaceToNames}.
@@ -1967,61 +2012,65 @@ public synchronized static boolean releaseService(String inName) {
    * @param inName
    *          Name of the service to unregister
    */
-  synchronized public static void unregister(String inName) {
-    String name = getFullName(inName);
-    log.info("unregister {}", name);
+  public static void unregister(String inName) {
+    synchronized (processLock) {
+      String name = getFullName(inName);
+      log.info("unregister {}", name);
 
-    // get reference from registry
-    ServiceInterface sw = registry.get(name);
-    if (sw == null) {
-      log.debug("{} already unregistered", name);
-      return;
-    }
+      // get reference from registry
+      ServiceInterface sw = registry.get(name);
+      if (sw == null) {
+        log.debug("{} already unregistered", name);
+        return;
+      }
 
-    // you have to send released before removing from registry
-    if (runtime != null) {
-      runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
-      // !!
-      // it should be FULLNAME !
-      // runtime.broadcast("released", inName);
-      String type = sw.getTypeKey();
-
-      boolean updatedServiceLists = false;
-
-      // check to see if any of our interfaces can fullfill requested ones
-      Set<String> myInterfaces = runtime.typeToInterface.get(type);
-      if (myInterfaces != null) {
-        for (String inter : myInterfaces) {
-          if (runtime.interfaceToNames.containsKey(inter)) {
-            runtime.interfaceToNames.get(inter).remove(name);
-            updatedServiceLists = true;
+      // you have to send released before removing from registry
+      if (runtime != null) {
+        runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
+        // !!
+        // it should be FULLNAME !
+        // runtime.broadcast("released", inName);
+        String type = sw.getTypeKey();
+
+        boolean updatedServiceLists = false;
+
+        // check to see if any of our interfaces can fullfill requested ones
+        Set<String> myInterfaces = runtime.typeToInterface.get(type);
+        if (myInterfaces != null) {
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).remove(name);
+              updatedServiceLists = true;
+            }
           }
         }
-      }
 
-      if (updatedServiceLists) {
-        runtime.invoke("publishInterfaceToNames");
-      }
+        if (updatedServiceLists) {
+          runtime.invoke("publishInterfaceToNames");
+        }
 
-    }
+      }
 
-    // FIXME - release autostarted peers ?
+      // FIXME - release autostarted peers ?
 
-    // last step - remove from registry by making new registry
-    // thread safe way
-    Map<String, ServiceInterface> removedService = new TreeMap<>();
-    for (String key : registry.keySet()) {
-      if (!name.equals(key)) {
-        removedService.put(key, registry.get(key));
+      // last step - remove from registry by making new registry
+      // thread safe way
+      Map<String, ServiceInterface> removedService = new TreeMap<>();
+      for (String key : registry.keySet()) {
+        if (!name.equals(key)) {
+          removedService.put(key, registry.get(key));
+        }
       }
-    }
-    registry = removedService;
+      registry = removedService;
 
-    // and config
-    RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
-    c.remove(CodecUtils.getShortName(name));
+      // and config
+      RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
+      if (c != null) {
+        c.remove(CodecUtils.getShortName(name));
+      }
 
-    log.info("released {}", name);
+      log.info("released {}", name);
+    }
   }
 
   /**
@@ -2092,12 +2141,14 @@ public static void releaseAll(boolean releaseRuntime, boolean block) {
 
     if (block) {
       processRelease(releaseRuntime);
+      ConfigUtils.reset();
     } else {
 
       new Thread() {
         @Override
         public void run() {
           processRelease(releaseRuntime);
+          ConfigUtils.reset();
         }
       }.start();
 
@@ -2110,45 +2161,51 @@ public void run() {
    * @param releaseRuntime
    *          Whether the Runtime should also be released
    */
-  synchronized static private void processRelease(boolean releaseRuntime) {
-
-    // reverse release to order of creation
-    Collection<ServiceInterface> local = getLocalServices().values();
-    List<ServiceInterface> ordered = new ArrayList<>(local);
-    ordered.removeIf(Objects::isNull);
-    Collections.sort(ordered);
-    Collections.reverse(ordered);
+  static private void processRelease(boolean releaseRuntime) {
+    synchronized (processLock) {
+      // reverse release to order of creation
+      Collection<ServiceInterface> local = getLocalServices().values();
+      List<ServiceInterface> ordered = new ArrayList<>(local);
+      ordered.removeIf(Objects::isNull);
+      Collections.sort(ordered);
+      Collections.reverse(ordered);
 
-    for (ServiceInterface sw : ordered) {
+      for (ServiceInterface sw : ordered) {
 
-      // no longer needed now - runtime "should be" guaranteed to be last
-      if (sw == Runtime.getInstance()) {
-        // skipping runtime
-        continue;
-      }
+        // no longer needed now - runtime "should be" guaranteed to be last
+        if (sw == Runtime.getInstance()) {
+          // skipping runtime
+          continue;
+        }
 
-      log.info("releasing service {}", sw.getName());
+        log.info("releasing service {}", sw.getName());
 
-      try {
-        sw.releaseService();
-      } catch (Exception e) {
-        runtime.error("%s threw while releasing", e);
-        log.error("release", e);
+        try {
+          sw.releaseService();
+        } catch (Exception e) {
+          if (runtime != null) {
+            runtime.error("%s threw while releasing", e);
+          }
+          log.error("release", e);
+        }
       }
-    }
 
-    // clean up remote ... the contract should
-    // probably be just remove their references - do not
-    // ask for them to be released remotely ..
-    // in thread safe way
+      // clean up remote ... the contract should
+      // probably be just remove their references - do not
+      // ask for them to be released remotely ..
+      // in thread safe way
 
-    if (releaseRuntime && runtime != null) {
-      runtime.releaseService();
-    } else {
-      // put runtime in new registry
-      Runtime.getInstance();
-      registry = new TreeMap<>();
-      registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
+      if (releaseRuntime) {
+        if (runtime != null) {
+          runtime.releaseService();
+        }
+        runtime = null;
+      } else {
+        // put runtime in new registry
+        Runtime.getInstance();
+        registry = new TreeMap<>();
+        registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
+      }
     }
   }
 
@@ -2643,75 +2700,78 @@ public String publishConfigFinished(String configName) {
    *          The type of the new service
    * @return The started service
    */
-  synchronized static public ServiceInterface start(String name, String type) {
-    try {
+  static public ServiceInterface start(String name, String type) {
+    synchronized (processLock) {
+      try {
 
-      ServiceInterface requestedService = Runtime.getService(name);
-      if (requestedService != null) {
-        log.info("requested service already exists");
-        if (requestedService.isRunning()) {
-          log.info("requested service already running");
-        } else {
-          requestedService.startService();
+        ServiceInterface requestedService = Runtime.getService(name);
+        if (requestedService != null) {
+          log.info("requested service already exists");
+          if (requestedService.isRunning()) {
+            log.info("requested service already running");
+          } else {
+            requestedService.startService();
+          }
+          return requestedService;
         }
-        return requestedService;
-      }
 
-      Plan plan = Runtime.load(name, type);
+        Plan plan = Runtime.load(name, type);
 
-      Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+        Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
 
-      if (services == null) {
-        Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration", name, type);
-        return null;
-      }
+        if (services == null) {
+          Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration", name, type);
+          return null;
+        }
 
-      requestedService = Runtime.getService(name);
+        requestedService = Runtime.getService(name);
 
-      // FIXME - does some order need to be maintained e.g. all children before
-      // parent
-      // breadth first, depth first, external order ordinal ?
-      for (ServiceInterface service : services.values()) {
-        if (service.getName().equals(name)) {
-          continue;
-        }
-        if (!Runtime.isStarted(service.getName())) {
-          service.startService();
+        // FIXME - does some order need to be maintained e.g. all children
+        // before
+        // parent
+        // breadth first, depth first, external order ordinal ?
+        for (ServiceInterface service : services.values()) {
+          if (service.getName().equals(name)) {
+            continue;
+          }
+          if (!Runtime.isStarted(service.getName())) {
+            service.startService();
+          }
         }
-      }
 
-      if (requestedService == null) {
-        Runtime.getInstance().error("could not start %s of type %s", name, type);
-        return null;
-      }
+        if (requestedService == null) {
+          Runtime.getInstance().error("could not start %s of type %s", name, type);
+          return null;
+        }
 
-      // getConfig() was problematic here for JMonkeyEngine
-      ServiceConfig sc = requestedService.getConfig();
-      // Map<String, Peer> peers = sc.getPeers();
-      // if (peers != null) {
-      // for (String p : peers.keySet()) {
-      // Peer peer = peers.get(p);
-      // log.info("peer {}", peer);
-      // }
-      // }
-      // recursive - start peers of peers of peers ...
-      Map<String, Peer> subPeers = sc.getPeers();
-      if (sc != null && subPeers != null) {
-        for (String subPeerKey : subPeers.keySet()) {
-          // IF AUTOSTART !!!
-          Peer subPeer = subPeers.get(subPeerKey);
-          if (subPeer.autoStart) {
-            Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
+        // getConfig() was problematic here for JMonkeyEngine
+        ServiceConfig sc = requestedService.getConfig();
+        // Map<String, Peer> peers = sc.getPeers();
+        // if (peers != null) {
+        // for (String p : peers.keySet()) {
+        // Peer peer = peers.get(p);
+        // log.info("peer {}", peer);
+        // }
+        // }
+        // recursive - start peers of peers of peers ...
+        Map<String, Peer> subPeers = sc.getPeers();
+        if (sc != null && subPeers != null) {
+          for (String subPeerKey : subPeers.keySet()) {
+            // IF AUTOSTART !!!
+            Peer subPeer = subPeers.get(subPeerKey);
+            if (subPeer.autoStart) {
+              Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
+            }
           }
         }
-      }
 
-      requestedService.startService();
-      return requestedService;
-    } catch (Exception e) {
-      runtime.error(e);
+        requestedService.startService();
+        return requestedService;
+      } catch (Exception e) {
+        runtime.error(e);
+      }
+      return null;
     }
-    return null;
   }
 
   /**
@@ -2721,32 +2781,36 @@ synchronized static public ServiceInterface start(String name, String type) {
    * @param name
    * @return
    */
-  synchronized static public ServiceInterface start(String name) {
-    if (Runtime.getService(name) != null) {
-      // already exists
-      ServiceInterface si = Runtime.getService(name);
-      if (!si.isRunning()) {
-        si.startService();
+  static public ServiceInterface start(String name) {
+    synchronized (processLock) {
+      if (Runtime.getService(name) != null) {
+        // already exists
+        ServiceInterface si = Runtime.getService(name);
+        if (!si.isRunning()) {
+          si.startService();
+        }
+        return si;
       }
-      return si;
-    }
-    Plan plan = Runtime.load(name, null);
-    Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
-    // FIXME - order ?
-    for (ServiceInterface service : services.values()) {
-      service.startService();
+      Plan plan = Runtime.load(name, null);
+      Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+      // FIXME - order ?
+      for (ServiceInterface service : services.values()) {
+        service.startService();
+      }
+      return Runtime.getService(name);
     }
-    return Runtime.getService(name);
   }
 
-  synchronized public static Plan load(String name, String type) {
-    try {
-      Runtime runtime = Runtime.getInstance();
-      return runtime.loadService(new Plan("runtime"), name, type, true, 0);
-    } catch (IOException e) {
-      runtime.error(e);
+  public static Plan load(String name, String type) {
+    synchronized (processLock) {
+      try {
+        Runtime runtime = Runtime.getInstance();
+        return runtime.loadService(new Plan("runtime"), name, type, true, 0);
+      } catch (IOException e) {
+        runtime.error(e);
+      }
+      return null;
     }
-    return null;
   }
 
   /**
@@ -2774,18 +2838,18 @@ public Runtime(String n, String id) {
 
         /**
          * This is used to run through all the possible services and determine
-         * if they have any missing dependencies.  If they do not they become "installed".
-         * The installed flag makes the gui do a crossout when a service type is selected.
+         * if they have any missing dependencies. If they do not they become
+         * "installed". The installed flag makes the gui do a crossout when a
+         * service type is selected.
          */
         for (MetaData metaData : serviceData.getServiceTypes()) {
           Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
           if (deps.size() == 0) {
             metaData.installed = true;
           } else {
-            warn("{} not installed", metaData.getSimpleName());
+            log.info("{} not installed", metaData.getSimpleName());
           }
         }
-        
       }
     }
 
@@ -4189,7 +4253,7 @@ static public String getFullName(String shortname) {
       return shortname;
     }
     // if nothing is supplied assume local
-    return String.format("%s@%s", shortname, Platform.getLocalInstance().getId());
+    return String.format("%s@%s", shortname, Runtime.getInstance().getId());
   }
 
   @Override
@@ -4482,18 +4546,20 @@ public static void main(String[] args) {
 
     try {
 
+      // loading args
       globalArgs = args;
-
       new CommandLine(options).parseArgs(args);
+      log.info("in args {}", Launcher.toString(args));
+      log.info("options {}", CodecUtils.toJson(options));
+      log.info("\n" + Launcher.banner);
+
+      // creating initial data/config directory
+      File cfgRoot = new File(ROOT_CONFIG_DIR);
+      cfgRoot.mkdirs();
 
       // initialize logging
       initLog();
 
-      log.info("in args {}", Launcher.toString(args));
-      log.info(CodecUtils.toJson(options));
-
-      log.info("\n" + Launcher.banner);
-
       // help and exit
       if (options.help) {
         mainHelp();
@@ -4503,45 +4569,23 @@ public static void main(String[] args) {
       // start.yml file is required, if not pre-existing
       // is created immediately. It contains static information
       // which needs to be available before a Runtime is created
-      File checkStartYml = new File("start.yml");
-      if (!checkStartYml.exists()) {
-        // save default
-        startYml = new StartYml();
-        String defaultStartFile = CodecUtils.toYaml(startYml);
-        FileIO.toFile("start.yml", defaultStartFile);
-      } else {
-        String yml = FileIO.toString("start.yml");
-        startYml = CodecUtils.fromYaml(yml, StartYml.class);
-      }
+      Runtime.startYml = ConfigUtils.loadStartYml();
 
-      // id always required - precedence
-      // if none supplied one will be generated
-      // if in start.yml it will be used
-      // if supplied by the command line it will be used
-      // command line has the highest precedence
-
-      Platform platform = Platform.getLocalInstance();
-      if (options.id != null) {
-        platform.setId(options.id);
-      } else if (startYml.id != null) {
-        platform.setId(startYml.id);
-      } else {
-        // no id set - should be first
-        // time mrl is started
-        String id = NameGenerator.getName();
-        platform.setId(id);
-        startYml.id = id;
-        FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
+      // resolve configName before starting getting runtime configuration
+      Runtime.configName = (startYml.enable) ? startYml.config : "default";
+      if (options.config != null) {
+        // cmd line options has the highest priority
+        Runtime.configName = options.config;
       }
 
-      if (options.virtual) {
-        Platform.setVirtual(true);
-      }
+      // start.yml is processed, config name is set, runtime config
+      // is resolved, now we can start instance
+      Runtime.getInstance();
 
-      // FIXME TEST THIS !! 0 length, single service, multiple !
       if (options.install != null) {
         // we start the runtime so there is a status publisher which will
         // display status updates from the repo install
+        log.info("requesting install");
         Repo repo = getInstance().getRepo();
         if (options.install.length == 0) {
           repo.install(LIBRARIES, (String) null);
@@ -4554,36 +4598,6 @@ public static void main(String[] args) {
         return;
       }
 
-      // if a you specify a config file it becomes the "base" of configuration
-      // inline flags will still override values
-      if (options.config != null) {
-        // if this is a valid config, it will load
-        setConfig(options.config);
-      } else {
-        // required directory to load any service
-        setConfig(startYml.config);
-      }
-
-      if (startYml.enable) {
-        Runtime.startConfig(startYml.config);
-      } else {
-        createAndStartServices(options.services);
-      }
-
-      if (options.invoke != null) {
-        invokeCommands(options.invoke);
-      }
-
-      if (options.connect != null) {
-        Runtime.getInstance().connect(options.connect);
-      }
-
-      if (options.autoUpdate) {
-        // initialize
-        // FIXME - use peer ?
-        Updater.main(args);
-      }
-
     } catch (Exception e) {
       log.error("runtime exception", e);
       Runtime.mainHelp();
@@ -4594,7 +4608,6 @@ public static void main(String[] args) {
 
   public static void initLog() {
     if (options != null) {
-      LoggingFactory.setLogFile(options.logFile);
       LoggingFactory.init(options.logLevel);
     } else {
       LoggingFactory.init("info");
@@ -4696,87 +4709,92 @@ static public ServiceInterface loadAndStart(String name, String type) {
    * @return
    * @throws IOException
    */
-  synchronized public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
+  public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
+    synchronized (processLock) {
 
-    if (plan == null) {
-      log.error("plan required to load a system");
-      return null;
-    }
+      if (plan == null) {
+        log.error("plan required to load a system");
+        return null;
+      }
 
-    log.info("loading - {} {} {}", name, type, level);
-    // from recursive memory definition
-    ServiceConfig sc = plan.get(name);
-
-    // HIGHEST PRIORITY - OVERRIDE WITH FILE
-    String configPath = runtime.getConfigPath();
-    String configFile = configPath + fs + name + ".yml";
-
-    // PRIORITY #1
-    // find if a current yml config file exists - highest priority
-    log.debug("priority #1 user's yml override {} ", configFile);
-    ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
-    if (fileSc != null) {
-      // if definition exists in file form, it overrides current memory one
-      sc = fileSc;
-    } else if (sc != null) {
-      // if memory config is available but not file
-      // we save it
-      String yml = CodecUtils.toYaml(sc);
-      FileIO.toFile(configFile, yml);
-    }
-
-    // special conflict case - type is specified, but its not the same as
-    // file version - in that case specified parameter type wins and overwrites
-    // config. User can force type by supplying one as a parameter, however, the
-    // recursive
-    // call other peer types will have name/file.yml definition precedence
-    if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
-      if (sc != null) {
-        warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
+      log.info("loading - {} {} {}", name, type, level);
+      // from recursive memory definition
+      ServiceConfig sc = plan.get(name);
+
+      // HIGHEST PRIORITY - OVERRIDE WITH FILE
+      String configPath = runtime.getConfigPath();
+      String configFile = configPath + fs + name + ".yml";
+
+      // PRIORITY #1
+      // find if a current yml config file exists - highest priority
+      log.debug("priority #1 user's yml override {} ", configFile);
+      ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
+      if (fileSc != null) {
+        // if definition exists in file form, it overrides current memory one
+        sc = fileSc;
+      } else if (sc != null) {
+        // if memory config is available but not file
+        // we save it
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
       }
-      ServiceConfig.getDefault(plan, name, type);
-      sc = plan.get(name);
 
-      // create new file if it didn't exist or overwrite it if new type is
-      // required
-      String yml = CodecUtils.toYaml(sc);
-      FileIO.toFile(configFile, yml);
-    }
+      // special conflict case - type is specified, but its not the same as
+      // file version - in that case specified parameter type wins and
+      // overwrites
+      // config. User can force type by supplying one as a parameter, however,
+      // the
+      // recursive
+      // call other peer types will have name/file.yml definition precedence
+      if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
+        if (sc != null) {
+          warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
+        }
+        ServiceConfig.getDefault(plan, name, type);
+        sc = plan.get(name);
 
-    if (sc == null && type == null) {
-      log.error("no local config and unknown type");
-      return plan;
-    }
+        // create new file if it didn't exist or overwrite it if new type is
+        // required
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
+      }
 
-    // finalize
-    if (sc != null) {
-      plan.put(name, sc);
-      // RECURSIVE load peers
-      Map<String, Peer> peers = sc.getPeers();
-      for (String peerKey : peers.keySet()) {
-        Peer peer = peers.get(peerKey);
-        // recursive depth load - parent and child need to be started
-        runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
+      if (sc == null && type == null) {
+        log.error("no local config and unknown type");
+        return plan;
       }
 
-      // valid service config at this point - now determine if its supposed to
-      // start or not
-      // if its level 0 then it was requested by user or config - so it needs to
-      // start
-      // if its not level 0 then it was loaded because peers were defined and
-      // appropriate config loaded
-      // peer.autoStart should determine if the peer starts if not explicitly
-      // requested by the
-      // user or config
-      if (level == 0 || start) {
-        plan.addRegistry(name);
+      // finalize
+      if (sc != null) {
+        plan.put(name, sc);
+        // RECURSIVE load peers
+        Map<String, Peer> peers = sc.getPeers();
+        for (String peerKey : peers.keySet()) {
+          Peer peer = peers.get(peerKey);
+          // recursive depth load - parent and child need to be started
+          runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
+        }
+
+        // valid service config at this point - now determine if its supposed to
+        // start or not
+        // if its level 0 then it was requested by user or config - so it needs
+        // to
+        // start
+        // if its not level 0 then it was loaded because peers were defined and
+        // appropriate config loaded
+        // peer.autoStart should determine if the peer starts if not explicitly
+        // requested by the
+        // user or config
+        if (level == 0 || start) {
+          plan.addRegistry(name);
+        }
+
+      } else {
+        log.info("could not load {} {} {}", name, type, level);
       }
 
-    } else {
-      log.info("could not load {} {} {}", name, type, level);
+      return plan;
     }
-
-    return plan;
   }
 
   /**
@@ -4846,45 +4864,30 @@ public String publishConfigLoaded(String name) {
     return name;
   }
 
-  public String setAllIds(String id) {
-    Platform.getLocalInstance().setId(id);
-    for (ServiceInterface si : getServices()) {
-      si.setId(id);
-    }
-    return id;
-  }
-
   @Override
-  public RuntimeConfig apply(RuntimeConfig c) {
-    super.apply(c);
-    config = c;
+  public RuntimeConfig apply(RuntimeConfig config) {
+    super.apply(config);
 
     setLocale(config.locale);
 
-    if (config.id != null) {
-      setAllIds(config.id);
+    if (config.id == null) {
+      config.id = NameGenerator.getName();
     }
 
     if (config.logLevel != null) {
       setLogLevel(config.logLevel);
     }
 
-    info("setting locale to %s", config.locale);
     if (config.virtual != null) {
       info("setting virtual to %b", config.virtual);
       setAllVirtual(config.virtual);
     }
 
-    if (config.enableCli) {
-      startInteractiveMode();
-      info("enabled cli");
-    } else {
-      stopInteractiveMode();
-      info("disabled cli");
-    }
+    // APPLYING A RUNTIME CONFIG DOES NOT PROCESS THE REGISTRY
+    // USE startConfig(name)
 
     broadcastState();
-    return c;
+    return config;
   }
 
   /**
@@ -4986,7 +4989,6 @@ public boolean saveService(String configName, String serviceName, String filenam
 
       // conditional boolean to flip and save a config name to start.yml ?
       if (startYml.enable) {
-        startYml.id = getId();
         startYml.config = configName;
         FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
       }
@@ -5019,26 +5021,6 @@ public boolean saveService(String configName, String serviceName, String filenam
     return false;
   }
 
-  public String setConfigName(String name) {
-    if (name != null && name.contains(fs)) {
-      error("invalid character " + fs + " in configuration name");
-      return configName;
-    }
-    if (name != null) {
-      configName = name.trim();
-    }
-
-    // for the moment the best way is to mandate
-    // a dir is created when a new config name is set
-    // because loading service are required to save config
-    // before starting
-    File configDir = new File(ROOT_CONFIG_DIR + fs + name);
-    configDir.mkdirs();
-
-    invoke("publishConfigList");
-    return name;
-  }
-
   public String getConfigName() {
     return configName;
   }
@@ -5056,13 +5038,34 @@ public boolean isProcessingConfig() {
    *          - config dir name under data/config/{config}
    * @return configName
    */
-  public static String setConfig(String configName) {
+  public static String setConfig(String name) {
+    if (name == null) {
+      log.error("config cannot be null");
+      if (runtime != null) {
+        runtime.error("config cannot be null");
+      }
+      return null;
+    }
+
+    if (name.contains(fs)) {
+      log.error("invalid character " + fs + " in configuration name");
+      if (runtime != null) {
+        runtime.error("invalid character " + fs + " in configuration name");
+      }
+      return name;
+    }
 
-    File configDir = new File(ROOT_CONFIG_DIR + fs + configName);
-    configDir.mkdirs();
+    configName = name.trim();
+
+    File configDir = new File(ROOT_CONFIG_DIR + fs + name);
+    if (!configDir.exists()) {
+      configDir.mkdirs();
+    }
+
+    if (runtime != null) {
+      runtime.invoke("publishConfigList");
+    }
 
-    Runtime runtime = Runtime.getInstance();
-    runtime.setConfigName(configName);
     return configName;
   }
 
@@ -5279,14 +5282,6 @@ public String getConfigPath() {
     return ROOT_CONFIG_DIR + fs + configName;
   }
 
-  @Override
-  public RuntimeConfig getConfig() {
-    config = super.getConfig();
-    config.locale = getLocaleTag();
-    config.virtual = isVirtual;
-    return config;
-  }
-
   /**
    * Gets a {serviceName}.yml file config from configName directory
    * 
@@ -5391,11 +5386,12 @@ public ServiceConfig getPeer(String sericeName, String peerKey) {
   /**
    * Removes a config set and all its files
    * 
-   * @param configName - name of config
+   * @param configName
+   *          - name of config
    */
   public static void removeConfig(String configName) {
     try {
-      log.info("removeing config");
+      log.info("removing config");
 
       File check = new File(ROOT_CONFIG_DIR + fs + configName);
 
@@ -5408,12 +5404,4 @@ public static void removeConfig(String configName) {
     }
   }
 
-  /**
-   * Method used to determine is runtime is running without starting it
-   * @return true if available
-   */
-  static public boolean isAvailable() {
-    return runtime != null && runtime.isRunning();
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 21deea126c..5e87cddb05 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -62,7 +62,8 @@
  * services are already APIs - perhaps a data API - same as service without the
  * message wrapper
  */
-public class WebGui extends Service<WebGuiConfig> implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
+public class WebGui extends Service<WebGuiConfig>
+    implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
 
   public static class LiveVideoStreamHandler implements Handler {
 
@@ -89,7 +90,7 @@ public void handle(AtmosphereResource r) {
       }
     }
   }
-  
+
   private final transient IncomingMsgQueue inMsgQueue = new IncomingMsgQueue();
 
   public static class Panel {
@@ -127,7 +128,7 @@ public Panel(String name, int x, int y, int z) {
    * needed to get the api key to select the appropriate api processor
    * 
    * @param uri
-   *          u
+   *            u
    * @return api key
    * 
    */
@@ -270,9 +271,9 @@ public boolean getAutoStartBrowser() {
    * String broadcast to specific client
    * 
    * @param uuid
-   *          u
+   *             u
    * @param str
-   *          s
+   *             s
    * 
    */
   public void broadcast(String uuid, String str) {
@@ -314,7 +315,9 @@ public Config.Builder getNettosphereConfig() {
         // cert.privateKey()).build();
 
         SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
-        SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK)
+        SslContext context = SslContextBuilder
+            .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
+            .sslProvider(SslProvider.JDK)
             .clientAuth(ClientAuth.NONE).build();
 
         configBuilder.sslContext(context);
@@ -493,7 +496,8 @@ public void handle(AtmosphereResource r) {
         } else if ((bodyData != null) && log.isDebugEnabled()) {
           logData = bodyData;
         }
-        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid);
+        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(),
+            request.getRequestURI(), logData, uuid);
       }
 
       // important persistent connections will have associated routes ...
@@ -571,7 +575,8 @@ public void handle(AtmosphereResource r) {
           }
 
           if (msg.containsHop(getId())) {
-            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
+            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender,
+                msg.name, msg.method);
             return;
           }
 
@@ -915,7 +920,7 @@ public void run() {
    * remotely control UI
    * 
    * @param panel
-   *          - the panel which has been moved or resized
+   *              - the panel which has been moved or resized
    */
   public void savePanel(Panel panel) {
     if (panel.name == null) {
@@ -1102,7 +1107,7 @@ public void releaseService() {
    * Default (false) is to use the CDN
    *
    * @param useLocalResources
-   *          - true uses local resources fals uses cdn
+   *                          - true uses local resources fals uses cdn
    */
   public void useLocalResources(boolean useLocalResources) {
     this.useLocalResources = useLocalResources;
@@ -1162,7 +1167,7 @@ public WebGuiConfig getConfig() {
 
   public WebGuiConfig apply(WebGuiConfig c) {
     super.apply(c);
-    
+
     if (c.port != null && (port != null && c.port.intValue() != port.intValue())) {
       setPort(c.port);
     }
@@ -1178,17 +1183,19 @@ public static void main(String[] args) {
 
     try {
 
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
-      Runtime.main(new String[] { "--install" });
-      
+      Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui","intro", "Intro", "python", "Python" });
+      // Runtime.main(new String[] {});
+      // Runtime.main(new String[] { "--install" });
+
       boolean done = true;
       if (done) {
         return;
       }
-      
+
       // Platform.setVirtual(true);
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui", "intro", "Intro", "python", "Python", "-c", "dev" });
-      // Runtime.startConfig("dev");      
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
+      // "intro", "Intro", "python", "Python", "-c", "dev" });
+      // Runtime.startConfig("dev");
 
       // Runtime.start("python", "Python");
       // Arduino arduino = (Arduino)Runtime.start("arduino", "Arduino");
@@ -1199,13 +1206,10 @@ public static void main(String[] args) {
       // webgui.setSsl(true);
       webgui.startService();
 
-
-      
       Runtime.start("python", "Python");
       // Runtime.start("intro", "Intro");
       // Runtime.start("i01", "InMoov2");
 
-    
       // Runtime.start("i01", "InMoov2");
       // Runtime.start("python", "Python");
       // Runtime.start("i01", "InMoov2");
@@ -1263,7 +1267,6 @@ public static void main(String[] args) {
        * Runtime.start("clock03", "Clock"); Runtime.start("clock04", "Clock");
        * Runtime.start("clock05", "Clock");
        */
-      Platform.setVirtual(true);
 
       // Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino");
       Servo pan = (Servo) Runtime.start("pan", "Servo");
@@ -1309,5 +1312,4 @@ public void onStopped(String name) {
   public void onReleased(String name) {
   }
 
-
 }
diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
index 87b9e4a1d1..d7572cd118 100644
--- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
@@ -1,31 +1,56 @@
 package org.myrobotlab.service.config;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import org.myrobotlab.framework.NameGenerator;
+import org.myrobotlab.framework.Plan;
+import org.myrobotlab.service.data.Locale;
+
 public class RuntimeConfig extends ServiceConfig {
 
   /**
    * instance id - important to be unique when connecting multiple
    * mrl instances together
    */
-  public String id;
+  public String id = NameGenerator.getName();
   
   /**
    * virtual hardware if enabled all services created will enable virtualization if applicable
    */
   public Boolean virtual = false;
-  public boolean enableCli = true;
-  public String logLevel = "info";
-  public String locale;
+    
+  /**
+   * Log level debug, info, warn, error
+   */
+  public String logLevel = "warn";
+  
+  /**
+   * Locale setting for the instance, initial default will be set by the default jvm/os
+   * through java.util.Locale.getDefault()
+   */
+  public String locale = Locale.getDefault().getTag();
   
-  // NEED THIS PRIVATE BUT CANNOT BE
+
+  /**
+   * Although this should be a set of unique services, it cannot be a LinkedHashSet
+   * because SnakeYml's interpretation would be a map with null values.  Instead
+   * its a protected member with accessors that prevent duplicates.
+   */
   public List<String> registry = new ArrayList<>();
     
   /**
    * Root of resource location
    */
   public String resource = "resource";
+  
+  
+  public Plan getDefault(Plan plan, String name) {
+    super.getDefault(plan, name);
+    return plan;
+  }
+  
 
   /**
    * add and remove a service using these methods and the uniqueness will be
diff --git a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
index 7ffd26fcd4..e26d28e732 100644
--- a/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
+++ b/src/test/java/org/myrobotlab/codec/CodecUtilsTest.java
@@ -186,25 +186,6 @@ public void testDefaultSerialization() {
     
   }
 
-  @Test
-  public void testNormalizeServiceName() {
-    Platform.getLocalInstance().setId("test-id");
-    assertEquals("runtime@test-id", CodecUtils.getFullName("runtime"));
-    assertEquals("runtime@test-id", CodecUtils.getFullName("runtime@test-id"));
-  }
-
-  @Test
-  public void testCheckServiceNameEqual() {
-    Platform.getLocalInstance().setId("test-id");
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime", "runtime@test-id"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime"));
-    assertTrue(CodecUtils.checkServiceNameEquality("runtime@test-id", "runtime@test-id"));
-    assertFalse(CodecUtils.checkServiceNameEquality("runtime", "runtime@not-corr-id"));
-    assertFalse(CodecUtils.checkServiceNameEquality("runtime@not-corr-id", "runtime"));
-
-  }
-  
   @Test
   public void testBase64() {
     // not a very comprehensive test, but a sanity check none the less.
diff --git a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
index 85f0d4f2ee..7afa011b8e 100644
--- a/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
+++ b/src/test/java/org/myrobotlab/framework/CmdOptionsTest.java
@@ -1,6 +1,7 @@
 package org.myrobotlab.framework;
-
+import org.junit.Ignore;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
@@ -9,10 +10,12 @@
 
 import org.junit.Test;
 import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.Runtime;
+import org.myrobotlab.service.config.ClockConfig;
 import org.slf4j.Logger;
 
 import picocli.CommandLine;
-
+@Ignore
 public class CmdOptionsTest {
 
   public final static Logger log = LoggerFactory.getLogger(CmdOptionsTest.class);
@@ -32,29 +35,36 @@ public void testGetOutputCmd() throws IOException {
     CmdOptions options = new CmdOptions();
     new CommandLine(options).parseArgs(new String[] {});
     // validate defaults
-    assertEquals(false, options.autoUpdate);
     assertNull(options.config);
-    assertNull(options.connect);
     assertEquals(0, options.services.size());
 
-    new CommandLine(options).parseArgs(new String[] { "--id", "raspi", "-s", "webgui", "WebGui", "clock01", "Clock" });
+    new CommandLine(options).parseArgs(new String[] {  "-s", "webgui", "WebGui", "clock01", "Clock" });
 
-    assertEquals("raspi", options.id);
     assertEquals(4, options.services.size());
 
     List<String> cmd = options.getOutputCmd();
     assertTrue(contains(cmd, "webgui"));
-    assertTrue(contains(cmd, "raspi"));
+    assertTrue(contains(cmd, "clock01"));
 
     log.info(CmdOptions.toString(cmd));
 
-    options = new CmdOptions();
-    new CommandLine(options).parseArgs(new String[] { "-a" });
-    assertEquals(true, options.autoUpdate);
-
+    Runtime.releaseAll(true, true);
     // test help
-
-    // test unmatched option
+    Runtime.main(new String[] { "--id", "test", "-s", "clockCmdTest", "Clock" });
+    assertNotNull(Runtime.getService("clockCmdTest"));
+    assertEquals("test", Runtime.getInstance().getId());
+
+    Runtime.releaseAll(true, true);
+    
+    Runtime.main(new String[] { "-c", "xxx", "-s", "clockCmdTest", "Clock" });
+    
+    ClockConfig clock = (ClockConfig)Runtime.getInstance().readServiceConfig("xxx", "clockCmdTest");
+    assertNotNull(clock);
+    assertNotNull(Runtime.getService("clockCmdTest"));
+    
+    Runtime.releaseAll(true, true);
+    
+    log.info("here");
 
   }
 
diff --git a/src/test/java/org/myrobotlab/framework/ConfigTest.java b/src/test/java/org/myrobotlab/framework/ConfigTest.java
index 4f8f1a9567..955a18e2ce 100644
--- a/src/test/java/org/myrobotlab/framework/ConfigTest.java
+++ b/src/test/java/org/myrobotlab/framework/ConfigTest.java
@@ -8,14 +8,8 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Comparator;
 
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -38,23 +32,18 @@
 import org.slf4j.Logger;
 
 public class ConfigTest extends AbstractTest {
-  
-  
+
   @BeforeClass
   public static void setUpBeforeClass() {
-      System.out.println("Runs before any test method in the class");
-  }
-
-  @AfterClass
-  public static void tearDownAfterClass() {
-      System.out.println("Runs after all test methods in the class");
+    // clean out services - reset
+    Runtime.releaseAll(true, true);
   }
 
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-      // clean our config directory
+    // clean our config directory
     Runtime.removeConfig(CONFIG_NAME);
     // set our config
     Runtime.setConfig(CONFIG_NAME);
@@ -62,9 +51,8 @@ public void setUp() throws IOException {
 
   @After
   public void tearDown() {
-      System.out.println("Runs after each test method");
+    System.out.println("Runs after each test method");
   }
-  
 
   // --- config set related ---
   // setConfigPath(fullpath)
@@ -90,32 +78,31 @@ public void tearDown() {
 
   final String CONFIG_PATH = "data" + File.separator + "config" + File.separator + CONFIG_NAME;
 
-
   @Test
   public void testStartNoConfig() throws Exception {
 
     Runtime runtime = Runtime.getInstance();
     assertNotNull(runtime);
-    
+
     // complete teardown, release runtime, block
     Runtime.releaseAll(true, true);
-    
+
     String[] names = Runtime.getServiceNames();
-    assertEquals("complete teardown should be 0", 0, names.length);
-    
+    assertEquals("after teardown, then using a runtime static - only 0 service 'runtime' should exist", 0, names.length);
+
     // nothing to start - should be empty config
     Runtime.startConfig(CONFIG_NAME);
-    
+
     // starting an empty config automatically needs a runtime, and runtime
     // by default starts the singleton security service
     names = Runtime.getServiceNames();
-    assertEquals("complete teardown should be 2 after trying to start a config runtime and security", 2, names.length);
-   
+    assertEquals("complete teardown should be 1 after trying to start a config runtime", 1, names.length);
+    Runtime.releaseAll(true, true);
   }
-  
+
   @Test
   public void testSwitchingPeer() throws IOException {
-    
+
     Runtime runtime = Runtime.getInstance();
     assertNotNull(runtime);
 
@@ -123,53 +110,53 @@ public void testSwitchingPeer() throws IOException {
     // to the current config directory
     Plan plan = Runtime.load("eyeTracking", "Tracking");
     assertNotNull(plan);
-    
+
     // load eyeTracking.yml config - verify default state
-    TrackingConfig eyeTracking = (TrackingConfig)runtime.getConfig(CONFIG_NAME, "eyeTracking");
+    TrackingConfig eyeTracking = (TrackingConfig) runtime.getConfig(CONFIG_NAME, "eyeTracking");
     TrackingConfig defaultTracking = new TrackingConfig();
     assertEquals("eyeTracking.yml values should be the same as default", defaultTracking.enabled, eyeTracking.enabled);
     assertEquals("eyeTracking.yml type should be the same as default", defaultTracking.type, eyeTracking.type);
 
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
     assertEquals("eyeTracking.yml values should be the same as default", defaultTracking.enabled, eyeTracking.enabled);
     assertEquals("eyeTracking.yml type should be the same as default", defaultTracking.type, eyeTracking.type);
-    
+
     // load single opencv
-    OpenCVConfig cv = (OpenCVConfig)Runtime.load("cv", "OpenCV").get("cv");
+    OpenCVConfig cv = (OpenCVConfig) Runtime.load("cv", "OpenCV").get("cv");
     // default capturing is false
     assertFalse(cv.capturing);
 
     // save as true
     cv.capturing = true;
     Runtime.saveConfig("cv", cv);
-    
+
     Runtime.load("pid", "Pid");
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
-    
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
+
     eyeTracking.getPeer("cv").name = "cv";
     Runtime.saveConfig("eyeTracking", eyeTracking);
-    
+
     // verify the peer was updated to cv
-    eyeTracking = (TrackingConfig)runtime.getConfig("eyeTracking");
-    cv = (OpenCVConfig)runtime.getPeerConfig("eyeTracking","cv");
+    eyeTracking = (TrackingConfig) runtime.getConfig("eyeTracking");
+    cv = (OpenCVConfig) runtime.getPeerConfig("eyeTracking", "cv");
     // from previous save
     assertTrue(cv.capturing);
 
   }
-  
+
   @Test
   public void testChangeType() throws IOException {
-    Runtime runtime = Runtime.getInstance();    
+    Runtime runtime = Runtime.getInstance();
     Runtime.load("mouth", "MarySpeech");
-    MarySpeechConfig mouth = (MarySpeechConfig)runtime.getConfig("mouth");
+    MarySpeechConfig mouth = (MarySpeechConfig) runtime.getConfig("mouth");
     mouth.listeners.add(new Listener("publishStartSpeaking", "fakeListener"));
     Runtime.saveConfig("mouth", mouth);
-    MarySpeechConfig mary = (MarySpeechConfig)runtime.getConfig("mouth");
+    MarySpeechConfig mary = (MarySpeechConfig) runtime.getConfig("mouth");
     assertNotNull(mary);
     assertEquals(1, mary.listeners.size());
     // save it
     runtime.changeType("mouth", "LocalSpeech");
-    LocalSpeechConfig local = (LocalSpeechConfig)runtime.getConfig("mouth");
+    LocalSpeechConfig local = (LocalSpeechConfig) runtime.getConfig("mouth");
     assertEquals("must have the listener", 1, local.listeners.size());
     assertTrue(local.listeners.get(0).listener.equals("fakeListener"));
   }
@@ -178,23 +165,23 @@ public void testChangeType() throws IOException {
   public void testInitialLoad() {
     Runtime runtime = Runtime.getInstance();
     Runtime.load("service", "Clock");
-    ClockConfig clock = (ClockConfig)runtime.getConfig("service");
+    ClockConfig clock = (ClockConfig) runtime.getConfig("service");
     assertNotNull(clock);
     // replace load
     Runtime.load("service", "Tracking");
-    TrackingConfig tracking = (TrackingConfig)runtime.getConfig("service");
+    TrackingConfig tracking = (TrackingConfig) runtime.getConfig("service");
     assertNotNull(tracking);
   }
-  
+
   @Test
   public void testChangePeerName() throws IOException {
     Runtime runtime = Runtime.getInstance();
     Plan plan = Runtime.load("pollyMouth", "Polly");
-    PollyConfig polly = (PollyConfig)plan.get("pollyMouth");    
+    PollyConfig polly = (PollyConfig) plan.get("pollyMouth");
     Runtime.load("i01", "InMoov2");
-    InMoov2Config i01 = (InMoov2Config)runtime.getConfig("i01");
+    InMoov2Config i01 = (InMoov2Config) runtime.getConfig("i01");
     // default
-    MarySpeechConfig mary = (MarySpeechConfig)runtime.getPeer("i01", "mouth");
+    MarySpeechConfig mary = (MarySpeechConfig) runtime.getPeer("i01", "mouth");
     assertNotNull(mary);
     polly.listeners = mary.listeners;
     Runtime.saveConfig("pollyMouth", polly);
@@ -202,48 +189,50 @@ public void testChangePeerName() throws IOException {
     peer.name = "pollyMouth";
     Runtime.saveConfig("i01", i01);
     // switch to pollyMouth
-    PollyConfig p = (PollyConfig)runtime.getPeer("i01", "mouth");
-    
+    PollyConfig p = (PollyConfig) runtime.getPeer("i01", "mouth");
+
     // FIXME - was going to test moving of subscriptions, however, unfortunately
-    // SpeechSynthesis services use a "recognizers" data instead of just simple subscriptions
+    // SpeechSynthesis services use a "recognizers" data instead of just simple
+    // subscriptions
     // This should be fixed in the future to use standard subscriptions
-    
-  }  
-  
+
+  }
+
   @Test
   public void testSimpleServiceStart() {
-    Clock clock = (Clock)Runtime.start("track", "Clock");
+    Runtime.releaseAll(true, true);
+    Clock clock = (Clock) Runtime.start("track", "Clock");
     clock.startClock();
     clock.releaseService();
     // better be a tracking service
-    LocalSpeech track = (LocalSpeech)Runtime.start("track", "LocalSpeech");
+    LocalSpeech track = (LocalSpeech) Runtime.start("track", "LocalSpeech");
     assertNotNull(track);
     track.releaseService();
     // better be a clock
-    clock = (Clock)Runtime.create("track", "Clock");
+    clock = (Clock) Runtime.create("track", "Clock");
     log.info("start");
   }
 
   @Test
   public void testPeers() {
-    InMoov2Head head = (InMoov2Head)Runtime.start("track", "InMoov2Head");
-    Servo neck = (Servo)Runtime.getService("track.neck");
+    Runtime.releaseAll(true, true);
+    InMoov2Head head = (InMoov2Head) Runtime.start("track", "InMoov2Head");
+    Servo neck = (Servo) Runtime.getService("track.neck");
     assertNotNull(neck);
     head.releaseService();
     assertNull(Runtime.getService("track.neck"));
-    
   }
-  
+
   @Test
   public void testSaveApply() throws IOException {
     Runtime runtime = Runtime.getInstance();
-    Servo neck = (Servo)Runtime.start("neck", "Servo");
+    Servo neck = (Servo) Runtime.start("neck", "Servo");
     ServoConfig config = neck.getConfig();
-    
+
     // Where config is "different" than member variables it
     // takes an apply(config) of the config to make the service
     // update its member variables, vs changing config and
-    // immediately getting the service behavior change.     
+    // immediately getting the service behavior change.
     config.idleTimeout = 5000;
     // the fact this takes and additional method to process
     // i think is legacy and should be changed for Servo to use
@@ -251,24 +240,23 @@ public void testSaveApply() throws IOException {
     neck.apply(config);
     neck.save();
     neck.releaseService();
-    neck = (Servo)Runtime.start("neck", "Servo");
-    assertTrue("preserved value", 5000  == neck.getConfig().idleTimeout);
+    neck = (Servo) Runtime.start("neck", "Servo");
+    assertTrue("preserved value", 5000 == neck.getConfig().idleTimeout);
 
-    Servo servo = (Servo)Runtime.start("servo", "Servo");
-    config = (ServoConfig)Runtime.load("default", "Servo").get("default");
+    Servo servo = (Servo) Runtime.start("servo", "Servo");
+    config = (ServoConfig) Runtime.load("default", "Servo").get("default");
     assertNull(config.idleTimeout);
     config.idleTimeout = 7000;
     Runtime.saveConfig("servo", config);
     servo.apply();
     assertTrue(servo.getConfig().idleTimeout == 7000);
-    
+
     config.idleTimeout = 8000;
     servo.apply(config);
     assertTrue(servo.getIdleTimeout() == 8000);
     servo.apply();
     assertTrue("filesystem servo.yml applied", servo.getIdleTimeout() == 7000);
-    
+
   }
-  
 
 }
\ No newline at end of file
diff --git a/src/test/java/org/myrobotlab/io/FileIOTest.java b/src/test/java/org/myrobotlab/io/FileIOTest.java
index a8700219a8..8eb14bedb5 100644
--- a/src/test/java/org/myrobotlab/io/FileIOTest.java
+++ b/src/test/java/org/myrobotlab/io/FileIOTest.java
@@ -134,11 +134,6 @@ public void testGluePaths() {
     assertEquals("/abc/def/", ret);
   }
 
-  @Test
-  public void testIsJar() {
-    assertFalse(FileIO.isJar());
-  }
-
   @Test
   public void testGetFileListString() throws IOException {
     String dir = FileIO.gluePaths(tempDir, "testGetFileListString");
@@ -187,6 +182,7 @@ public void testToInputStreamString() throws IOException {
     InputStream ios = FileIO.toInputStream("This is some data that got turned into a stream");
     String data = FileIO.toString(ios);
     assertEquals("This is some data that got turned into a stream", data);
+    ios.close();
   }
 
   @Test
diff --git a/src/test/java/org/myrobotlab/service/RuntimeTest.java b/src/test/java/org/myrobotlab/service/RuntimeTest.java
index a20a13db38..e2a01278c1 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeTest.java
@@ -26,8 +26,8 @@ public class RuntimeTest extends AbstractTest {
   public final static Logger log = LoggerFactory.getLogger(RuntimeTest.class);
 
   @Before
-  public void setUp() {
-    // LoggingFactory.init("WARN");
+  public void beforeTest() {
+    Runtime.releaseAll(true, true);
   }
 
   @Test
@@ -92,9 +92,6 @@ public void testGetUptime() {
   @Test
   public void testRuntimeLocale() {
 
-    long curr = 1479044758691L;
-    Date d = new Date(curr);
-
     Runtime runtime = Runtime.getInstance();
     runtime.setLocale("fr-FR");
     assertEquals("expecting concat fr-FR", "fr-FR", runtime.getLocale().getTag());
@@ -105,16 +102,6 @@ public void testRuntimeLocale() {
 
   }
   
-  @Test 
-  public void testRuntimeIsAvailable() {
-    Runtime.getInstance();
-    assertTrue(Runtime.isAvailable());
-    Runtime.releaseAll(true, true);
-    assertFalse(Runtime.isAvailable());
-    Runtime.getInstance();
-    assertTrue(Runtime.isAvailable());
-  }
-  
 
   @Test
   public void testGetDescribeMessage() {
diff --git a/src/test/java/org/myrobotlab/service/SerialTest.java b/src/test/java/org/myrobotlab/service/SerialTest.java
index db1f10e315..63aeaa5cb5 100644
--- a/src/test/java/org/myrobotlab/service/SerialTest.java
+++ b/src/test/java/org/myrobotlab/service/SerialTest.java
@@ -53,7 +53,7 @@ public static Set<Thread> getDeadThreads() {
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
     // LoggingFactory.init("WARN");
-    Platform.setVirtual(true);
+    Runtime.getInstance().setVirtual(true);
 
     log.info("setUpBeforeClass");
 

From 46af60ce4e0fbfe789947da22f0538fba02815c2 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 12 Feb 2024 06:26:04 -0800
Subject: [PATCH 047/131] framework

---
 .../java/org/myrobotlab/service/InMoov2.java  |  2 +-
 .../org/myrobotlab/framework/ServiceTest.java | 46 -------------------
 2 files changed, 1 insertion(+), 47 deletions(-)
 delete mode 100644 src/test/java/org/myrobotlab/framework/ServiceTest.java

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 5f4233844c..46ab187b5b 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -1877,7 +1877,7 @@ public SpeechSynthesis startMouth() {
     broadcastState();
 
     speakBlocking(get("STARTINGMOUTH"));
-    if (Platform.isVirtual()) {
+    if (isVirtual()) {
       speakBlocking(get("STARTINGVIRTUALHARD"));
     }
     speakBlocking(get("WHATISTHISLANGUAGE"));
diff --git a/src/test/java/org/myrobotlab/framework/ServiceTest.java b/src/test/java/org/myrobotlab/framework/ServiceTest.java
deleted file mode 100644
index a9f180362c..0000000000
--- a/src/test/java/org/myrobotlab/framework/ServiceTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.myrobotlab.framework;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.List;
-import java.util.Map;
-
-import org.junit.Test;
-import org.myrobotlab.service.config.ServiceConfig;
-import org.myrobotlab.test.AbstractTest;
-
-public class ServiceTest extends AbstractTest {
-
-    public static class TestService extends Service<ServiceConfig> {
-
-        private static final long serialVersionUID = 1L;
-
-        /**
-         * Constructor of service, reservedkey typically is a services name and inId
-         * will be its process id
-         *
-         * @param reservedKey the service name
-         * @param inId        process id
-         */
-        public TestService(String reservedKey, String inId) {
-            super(reservedKey, inId);
-        }
-    }
-
-    @Test
-    public void testConfigListenerFiltering() {
-        Platform.getLocalInstance().setId("test-id");
-        TestService t = new TestService("test", "test-id");
-        List<MRLListener> listeners = List.of(
-                new MRLListener("meth", "webgui@webgui-client", "onMeth"),
-                new MRLListener("meth", "random@test-id", "onMeth"),
-                new MRLListener("meth", "random2@test-2-id", "onMeth")
-        );
-        t.apply(new ServiceConfig());
-        t.outbox.notifyList = Map.of("meth", listeners);
-        List<ServiceConfig.Listener> filtered = t.getFilteredConfig().listeners;
-        assertEquals("random", filtered.get(0).listener);
-        assertEquals("random2@test-2-id", filtered.get(1).listener);
-        t.getFilteredConfig();
-    }
-}

From e895567d8c5fd68e9c6fd6c8003c1d2ea822389c Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 12 Feb 2024 07:40:18 -0800
Subject: [PATCH 048/131] let framework process inmoov2 releaseService

---
 .vscode/settings.json                         |  2 +-
 .../org/myrobotlab/service/InMoov2Arm.java    | 53 ++-------------
 .../org/myrobotlab/service/InMoov2Hand.java   | 58 ----------------
 .../org/myrobotlab/service/InMoov2Head.java   | 66 -------------------
 .../org/myrobotlab/service/InMoov2Torso.java  | 55 +---------------
 .../service/interfaces/ServoControl.java      |  1 -
 6 files changed, 8 insertions(+), 227 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index c0d2e88e12..651c18245e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -17,7 +17,7 @@
 	"jest.coverageFormatter": "GutterFormatter",
 	"editor.formatOnSave": true,
 	"editor.codeActionsOnSave": {
-	  "source.organizeImports": true
+		"source.organizeImports": "explicit"
 	},
 	"typescript.tsdk": "./app/node_modules/typescript/lib",
 	"typescript.enablePromptUseWorkspaceTsdk": true,
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
index 1b4b45e6b5..e676c52878 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
@@ -90,7 +90,7 @@ public static DHRobotArm getDHRobotArm(String name, String side) {
 
     return arm;
   }
-  
+
   @Deprecated /* use onMove(map) */
   public void onMoveArm(HashMap<String, Double> map) {
     onMove(map);
@@ -100,7 +100,6 @@ public void onMove(Map<String, Double> map) {
     moveTo(map.get("bicep"), map.get("rotate"), map.get("shoulder"), map.get("omoplate"));
   }
 
-
   /**
    * peer services FIXME - framework should always - startPeers() unless
    * configured not to
@@ -125,23 +124,6 @@ public void startService() {
     shoulder = (ServoControl) startPeer("shoulder");
     omoplate = (ServoControl) startPeer("omoplate");
   }
-  
-  @Override
-  public void stopService() {
-    super.stopService();
-    if (bicep != null) {
-      ((Service)bicep).stopService();
-    }
-    if (rotate != null) {
-      ((Service)rotate).stopService();
-    }
-    if (shoulder != null) {
-      ((Service)shoulder).stopService();
-    }
-    if (omoplate != null) {
-      ((Service)omoplate).stopService();
-    }
-  }
 
   @Override
   public void broadcastState() {
@@ -210,8 +192,8 @@ public ServoControl getRotate() {
 
   public String getScript(String service) {
     String side = getName().contains("left") ? "left" : "right";
-    return String.format("%s.moveArm(\"%s\",%.0f,%.0f,%.0f,%.0f)\n", service, side, bicep.getCurrentInputPos(), rotate.getCurrentInputPos(),
-        shoulder.getCurrentInputPos(), omoplate.getCurrentInputPos());
+    return String.format("%s.moveArm(\"%s\",%.0f,%.0f,%.0f,%.0f)\n", service, side, bicep.getCurrentInputPos(), rotate.getCurrentInputPos(), shoulder.getCurrentInputPos(),
+        omoplate.getCurrentInputPos());
   }
 
   public ServoControl getShoulder() {
@@ -301,31 +283,6 @@ public void onJointAngles(Map<String, Double> angleMap) {
     }
   }
 
-  // FIXME - framework should auto-release - unless configured not to
-  @Override
-  public void releaseService() {
-    try {
-      disable();
-      
-      if (bicep != null) {
-        ((Service)bicep).releaseService();
-      }
-      if (rotate != null) {
-        ((Service)rotate).releaseService();
-      }
-      if (shoulder != null) {
-        ((Service)shoulder).releaseService();
-      }
-      if (omoplate != null) {
-        ((Service)omoplate).releaseService();
-      }
-      
-      super.releaseService();
-    } catch (Exception e) {
-      error(e);
-    }
-  }
-
   public void rest() {
     if (bicep != null)
       bicep.rest();
@@ -492,7 +449,7 @@ public void waitTargetPos() {
     if (omoplate != null)
       omoplate.waitTargetPos();
   }
-  
+
   public static void main(String[] args) {
     LoggingFactory.init(Level.INFO);
 
@@ -501,7 +458,7 @@ public static void main(String[] args) {
       Runtime.main(new String[] { "--log-level", "info", "-s", "inmoov2arm", "InMoov2Arm" });
       // Runtime.main(new String[] {});
       // Runtime.main(new String[] { "--install" });
-      InMoov2Arm arm = (InMoov2Arm)Runtime.start("inmoov2arm", "InMoov2Arm");
+      InMoov2Arm arm = (InMoov2Arm) Runtime.start("inmoov2arm", "InMoov2Arm");
       arm.releaseService();
 
       boolean done = true;
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
index 06b6d3b459..f7fdadebbc 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
@@ -567,64 +567,6 @@ public List<String> refreshControllers() {
     return controllers;
   }
 
-  public void release() {
-    disable();
-  }
-  
-  @Override
-  public void stopService() {
-    disable();
-    if (thumb != null) {
-      ((Service)thumb).stopService();
-    }
-    if (index != null) {
-      ((Service)index).stopService();
-    }
-    if (majeure != null) {
-      ((Service)majeure).stopService();
-    }
-    if (ringFinger != null) {
-      ((Service)ringFinger).stopService();
-    }
-    if (pinky != null) {
-      ((Service)pinky).stopService();
-    }
-    if (wrist != null) {
-      ((Service)wrist).stopService();
-    }
-    super.stopService();
-  }
-
-  @Override
-  public void releaseService() {
-    try {
-      disable();
-      
-      if (thumb != null) {
-        ((Service)thumb).releaseService();
-      }
-      if (index != null) {
-        ((Service)index).releaseService();
-      }
-      if (majeure != null) {
-        ((Service)majeure).releaseService();
-      }
-      if (ringFinger != null) {
-        ((Service)ringFinger).releaseService();
-      }
-      if (pinky != null) {
-        ((Service)pinky).releaseService();
-      }
-      if (wrist != null) {
-        ((Service)wrist).releaseService();
-      }
-      
-      super.releaseService();
-    } catch (Exception e) {
-      error(e);
-    }
-  }
-
   public void rest() {
     if (thumb != null)
       thumb.rest();
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java
index da9052ca65..1e98970f78 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Head.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java
@@ -359,72 +359,6 @@ public void waitTargetPos() {
   public void release() {
     disable();
   }
-  
-  @Override
-  public void stopService() {
-
-    if (jaw != null) {
-      ((Service)jaw).stopService();
-    }
-    if (eyeX != null) {
-      ((Service)eyeX).stopService();
-    }
-    if (eyeY != null) {
-      ((Service)eyeY).stopService();
-    }
-    if (neck != null) {
-      ((Service)neck).stopService();
-    }
-    if (rothead != null) {
-      ((Service)rothead).stopService();
-    }
-    if (rollNeck != null) {
-      ((Service)rollNeck).stopService();
-    }
-    if (eyelidLeft != null) {
-      ((Service)eyelidLeft).stopService();
-    }
-    if (eyelidRight != null) {
-      ((Service)eyelidRight).stopService();
-    }
-    
-    super.stopService();
-  }
-  
-  
-  
-
-  @Override
-  public void releaseService() {
-    disable();
-    
-    if (jaw != null) {
-      ((Service)jaw).releaseService();
-    }
-    if (eyeX != null) {
-      ((Service)eyeX).releaseService();
-    }
-    if (eyeY != null) {
-      ((Service)eyeY).releaseService();
-    }
-    if (neck != null) {
-      ((Service)neck).releaseService();
-    }
-    if (rothead != null) {
-      ((Service)rothead).releaseService();
-    }
-    if (rollNeck != null) {
-      ((Service)rollNeck).releaseService();
-    }
-    if (eyelidLeft != null) {
-      ((Service)eyelidLeft).releaseService();
-    }
-    if (eyelidRight != null) {
-      ((Service)eyelidRight).releaseService();
-    }
-    
-    super.releaseService();
-  }
 
   public void rest() {
     // initial positions
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
index 3754e43a3c..efdb127957 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
@@ -36,59 +36,10 @@ public InMoov2Torso(String n, String id) {
   @Override
   public void startService() {
     super.startService();
-    
     topStom = (ServoControl) getPeer("topStom");
     midStom = (ServoControl) getPeer("midStom");
     lowStom = (ServoControl) getPeer("lowStom");
   }
-  
-  @Override
-  public void stopService() {
-    disable();
-    
-    if (topStom != null) {
-      ((Service)topStom).stopService();
-    }
-    
-    if (midStom != null) {
-      ((Service)midStom).stopService();
-    }
-
-    if (lowStom != null) {
-      ((Service)lowStom).stopService();
-    }
-
-    super.stopService();
-  }
-
-  @Override
-  public void releaseService() {
-    try {
-      disable();
-      
-      
-      if (topStom != null) {
-        ((Service)topStom).releaseService();
-      }
-      
-      if (midStom != null) {
-        ((Service)midStom).releaseService();
-      }
-
-      if (lowStom != null) {
-        ((Service)lowStom).releaseService();
-      }
-
-
-      topStom = null;
-      midStom = null;
-      lowStom = null;
-
-      super.releaseService();
-    } catch (Exception e) {
-      error(e);
-    }
-  }
 
   public void enable() {
     if (topStom != null)
@@ -126,7 +77,7 @@ public void disable() {
     if (lowStom != null)
       lowStom.disable();
   }
-  
+
   @Deprecated /* use onMove(map) */
   public void onMoveTorso(HashMap<String, Double> map) {
     onMove(map);
@@ -136,7 +87,6 @@ public void onMove(Map<String, Double> map) {
     moveTo(map.get("topStom"), map.get("midStom"), map.get("lowStom"));
   }
 
-
   public long getLastActivityTime() {
     long minLastActivity = Math.max(topStom.getLastActivityTime(), midStom.getLastActivityTime());
     minLastActivity = Math.max(minLastActivity, lowStom.getLastActivityTime());
@@ -144,8 +94,7 @@ public long getLastActivityTime() {
   }
 
   public String getScript(String inMoovServiceName) {
-    return String.format("%s.moveTorso(%.0f,%.0f,%.0f)\n", inMoovServiceName, topStom.getCurrentInputPos(), midStom.getCurrentInputPos(),
-        lowStom.getCurrentInputPos());
+    return String.format("%s.moveTorso(%.0f,%.0f,%.0f)\n", inMoovServiceName, topStom.getCurrentInputPos(), midStom.getCurrentInputPos(), lowStom.getCurrentInputPos());
   }
 
   public void moveTo(Double topStomPos, Double midStomPos, Double lowStomPos) {
diff --git a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
index 1e140ce167..23a9ee9629 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
@@ -404,7 +404,6 @@ public interface ServoControl extends AbsolutePositionControl, EncoderListener,
   /**
    * disable speed control and move the servos at full speed.
    */
-  @Deprecated /* implement setSpeed(null) */
   void fullSpeed();
 
 }

From 237a5152da9bc2d9b86ed454f799c442a1a6b5ad Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 06:57:54 -0800
Subject: [PATCH 049/131] unit test updates

---
 .../org/myrobotlab/framework/repo/Repo.java   |   6 +-
 .../myrobotlab/framework/BlockingTest.java    |   4 +-
 .../myrobotlab/framework/repo/RepoTest.java   |   4 +
 .../org/myrobotlab/service/ArduinoTest.java   |   1 -
 .../service/RuntimeProcessTest.java           |   5 +-
 .../service/VirtualArduinoTest.java           |   4 +
 .../org/myrobotlab/test/AbstractTest.java     | 130 ++++++------------
 7 files changed, 60 insertions(+), 94 deletions(-)

diff --git a/src/main/java/org/myrobotlab/framework/repo/Repo.java b/src/main/java/org/myrobotlab/framework/repo/Repo.java
index 9262834da1..d8b6863c16 100644
--- a/src/main/java/org/myrobotlab/framework/repo/Repo.java
+++ b/src/main/java/org/myrobotlab/framework/repo/Repo.java
@@ -324,10 +324,10 @@ public Set<ServiceDependency> getUnfulfilledDependencies(String[] types) {
           }
         }
       }
-      
+
       // Plan plan = ServiceConfig.getDefault(type.toLowerCase(), type);
       ServiceConfig sc = ServiceConfig.getDefaultServiceConfig(type);
-      
+
       Map<String, Peer> peers = sc.getPeers();
       if (peers != null) {
         for (String key : peers.keySet()) {
@@ -496,7 +496,7 @@ public void load() {
         }
 
       } else {
-        log.info("{} not found", getRepoPath());
+        log.info("{} not found", f.getAbsolutePath());
       }
 
     } catch (Exception e) {
diff --git a/src/test/java/org/myrobotlab/framework/BlockingTest.java b/src/test/java/org/myrobotlab/framework/BlockingTest.java
index b4ce03e8da..3b9788645b 100644
--- a/src/test/java/org/myrobotlab/framework/BlockingTest.java
+++ b/src/test/java/org/myrobotlab/framework/BlockingTest.java
@@ -28,13 +28,13 @@ public void blockingTest() throws Exception {
 
     Message msg = Message.createMessage("thower07", "catcher07", "onInt", 3);
     Integer ret = (Integer)thower07.sendBlocking(msg, null);
-    assertEquals(simpleName, 3, (int)ret);
+    assertEquals(3, (int)ret);
 
     long startTime = System.currentTimeMillis();
     msg = Message.createMessage("thower07", "catcher07", "waitForThis", new Object[] {7, 1000});
     ret = (Integer)thower07.sendBlocking(msg, null);
     assertTrue("1s process", System.currentTimeMillis() - startTime > 500);
-    assertEquals(simpleName, 7, (int)ret);
+    assertEquals(7, (int)ret);
 
     Runtime.release("catcher07");
     Runtime.release("thower07");
diff --git a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
index f2715dc90d..d0e2c57735 100644
--- a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
+++ b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
@@ -28,6 +28,10 @@ public static void lastCleanup() {
     repo.clear();
     installed = false;
   }
+  
+  public String getName() {
+    return "RepoTest";
+  }
 
   @Override
   public void broadcastStatus(Status status) {
diff --git a/src/test/java/org/myrobotlab/service/ArduinoTest.java b/src/test/java/org/myrobotlab/service/ArduinoTest.java
index 7a6422d29d..00cee5e0a7 100644
--- a/src/test/java/org/myrobotlab/service/ArduinoTest.java
+++ b/src/test/java/org/myrobotlab/service/ArduinoTest.java
@@ -62,7 +62,6 @@ private void assertVirtualPinValue(VirtualArduino virtual, int address, int valu
     }
   }
 
-  @Override
   public String getName() {
     return "arduinoTest";
   }
diff --git a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
index 07e1775110..4bdb93fe2c 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
@@ -21,12 +21,15 @@ public class RuntimeProcessTest extends AbstractTest {
 
   @Before
   public void setUp() {
-    // LoggingFactory.init("WARN");
   }
 
   public boolean contains(ByteArrayOutputStream out, String str) {
     return new String(out.toByteArray()).contains(str);
   }
+  
+  public String getName() {
+    return "RuntimeProcessTest";
+  }
 
   @Test
   public void cliTest() throws Exception {
diff --git a/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java b/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
index ba1d028096..110ff1ac4a 100755
--- a/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
+++ b/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
@@ -30,6 +30,10 @@ public Service createService() {
     VirtualArduino service = (VirtualArduino) Runtime.start("virtualArduino", "VirtualArduino");
     return service;
   }
+  
+  public String getName() {
+    return "VirtualArduinoTest";
+  }
 
   @Override
   public void testService() throws Exception {
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index d782797c9b..4fd42b3bb9 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -14,82 +14,56 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Rule;
-import org.junit.rules.TestName;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
 import org.myrobotlab.codec.CodecUtils;
-import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
 import org.myrobotlab.service.config.RuntimeConfig;
 import org.slf4j.Logger;
 
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
 public class AbstractTest {
 
   /** cached network test value for tests */
-  static Boolean hasInternet = null;
+  protected static Boolean hasInternet = null;
 
   protected static boolean installed = false;
 
-  public final static Logger log = LoggerFactory.getLogger(AbstractTest.class);
-
-  static private boolean logWarnTestHeader = false;
-
-  private static boolean releaseRemainingThreads = false;
-
-  protected transient Queue<Object> queue = new LinkedBlockingQueue<>();
-
-  static transient Set<Thread> threadSetStart = null;
+  protected final static Logger log = LoggerFactory.getLogger(AbstractTest.class);
 
-  protected Set<Attachable> attached = new HashSet<>();
+  protected static boolean releaseRemainingThreads = false;
 
-  @Rule
-  public final TestName testName = new TestName();
-
-  static public String simpleName;
+  protected static transient Set<Thread> threadSetStart = null;
 
-  private static boolean lineFeedFooter = true;
-  
   @Rule
   public TestWatcher watchman = new TestWatcher() {
-      @Override
-      protected void starting(Description description) {
-          System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
-      }
+    @Override
+    protected void starting(Description description) {
+      System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
+    }
 
-      @Override
-      protected void succeeded(Description description) {
-         // System.out.println("Succeeded: " + description.getMethodName());
-      }
+    @Override
+    protected void succeeded(Description description) {
+      // System.out.println("Succeeded: " + description.getMethodName());
+    }
 
-      @Override
-      protected void failed(Throwable e, Description description) {
-          System.out.println("Failed: " + description.getMethodName());
-      }
+    @Override
+    protected void failed(Throwable e, Description description) {
+      System.out.println("Failed: " + description.getMethodName());
+    }
 
-      @Override
-      protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
-          System.out.println("Skipped: " + description.getMethodName());
-      }
+    @Override
+    protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
+      System.out.println("Skipped: " + description.getMethodName());
+    }
 
-      @Override
-      protected void finished(Description description) {
-          System.out.println("Finished: " + description.getMethodName());
-      }
+    @Override
+    protected void finished(Description description) {
+      System.out.println("Finished: " + description.getMethodName());
+    }
   };
 
-  public String getSimpleName() {
-    return simpleName;
-  }
-
-  public String getName() {
-    return testName.getMethodName();
-  }
-
   static public boolean hasInternet() {
     if (hasInternet == null) {
       hasInternet = Runtime.hasInternet();
@@ -120,23 +94,23 @@ public static void main(String[] args) {
 
   @BeforeClass
   public static void setUpAbstractTest() throws Exception {
-    
+
     // setup runtime resource = src/main/resources/resource
     File runtimeYml = new File("data/config/default/runtime.yml");
-//    if (!runtimeYml.exists()) {
-      runtimeYml.getParentFile().mkdirs();
-      RuntimeConfig rc = new RuntimeConfig();
-      rc.resource = "src/main/resources/resource";
-      String yml = CodecUtils.toYaml(rc);
-      
-      FileOutputStream fos = null;
-      fos = new FileOutputStream(runtimeYml);
-      fos.write(yml.getBytes());
-      fos.close();
-      
-//    }
-
-      Runtime.getInstance().setVirtual(true);
+    // if (!runtimeYml.exists()) {
+    runtimeYml.getParentFile().mkdirs();
+    RuntimeConfig rc = new RuntimeConfig();
+    rc.resource = "src/main/resources/resource";
+    String yml = CodecUtils.toYaml(rc);
+
+    FileOutputStream fos = null;
+    fos = new FileOutputStream(runtimeYml);
+    fos.write(yml.getBytes());
+    fos.close();
+
+    // }
+
+    Runtime.getInstance().setVirtual(true);
 
     String junitLogLevel = System.getProperty("junit.logLevel");
     if (junitLogLevel != null) {
@@ -171,16 +145,7 @@ public static void sleep(long sleepTimeMs) {
   @AfterClass
   public static void tearDownAbstractTest() throws Exception {
     log.info("tearDownAbstractTest");
-
     releaseServices();
-
-    if (logWarnTestHeader) {
-      log.warn("=========== finished test {} ===========", simpleName);
-    }
-
-    if (lineFeedFooter) {
-      System.out.println();
-    }
   }
 
   static protected void installAll() {
@@ -197,8 +162,7 @@ static protected void installAll() {
    */
   public static void releaseServices() {
 
-    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(),
-        Arrays.toString(Runtime.getServiceNames()));
+    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(), Arrays.toString(Runtime.getServiceNames()));
 
     // release all including runtime - be careful of default runtime.yml
     Runtime.releaseAll(true, true);
@@ -212,8 +176,7 @@ public static void releaseServices() {
     Set<Thread> threadSetEnd = Thread.getAllStackTraces().keySet();
     Set<String> threadsRemaining = new TreeSet<>();
     for (Thread thread : threadSetEnd) {
-      if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName())
-          && !"runtime".equals(thread.getName())) {
+      if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName()) && !"runtime".equals(thread.getName())) {
         if (releaseRemainingThreads) {
           log.warn("interrupting thread {}", thread.getName());
           thread.interrupt();
@@ -236,13 +199,6 @@ public static void releaseServices() {
     // Arrays.toString(Runtime.getServiceNames()));
   }
 
-  public AbstractTest() {
-    simpleName = this.getClass().getSimpleName();
-    if (logWarnTestHeader) {
-      log.info("=========== starting test {} ===========", this.getClass().getSimpleName());
-    }
-  }
-
   public void setVirtual() {
     Runtime.getInstance().setVirtual(true);
   }

From 95b3ddb31d6e860b036793db2b6992ec13d2bba6 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 07:24:48 -0800
Subject: [PATCH 050/131] stop cli when runtime is released

---
 .../java/org/myrobotlab/service/Runtime.java  |  3 +++
 .../org/myrobotlab/test/AbstractTest.java     | 21 +++++++------------
 2 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index e838cf6c52..49c27cff75 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -3446,6 +3446,9 @@ public void releaseService() {
       runtime.stopService();
       runtime.stopInteractiveMode();
       runtime.getRepo().removeStatusPublishers();
+      if (cli != null) {
+        cli.stop();
+      }
       registry = new TreeMap<>();
     }
     synchronized (INSTANCE_LOCK) {
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 4fd42b3bb9..7279a8fe98 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -25,15 +25,20 @@
 
 public class AbstractTest {
 
-  /** cached network test value for tests */
+  /**
+   * cached network test value for tests
+   */
   protected static Boolean hasInternet = null;
 
+  /**
+   * Install dependencies once per process, same process
+   * will not check.  A new process will use the libraries/serviceData.json
+   * to determine if deps are satisfied
+   */
   protected static boolean installed = false;
 
   protected final static Logger log = LoggerFactory.getLogger(AbstractTest.class);
 
-  protected static boolean releaseRemainingThreads = false;
-
   protected static transient Set<Thread> threadSetStart = null;
 
   @Rule
@@ -177,17 +182,7 @@ public static void releaseServices() {
     Set<String> threadsRemaining = new TreeSet<>();
     for (Thread thread : threadSetEnd) {
       if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName()) && !"runtime".equals(thread.getName())) {
-        if (releaseRemainingThreads) {
-          log.warn("interrupting thread {}", thread.getName());
-          thread.interrupt();
-          /*
-           * if (useDeprecatedThreadStop) { thread.stop(); }
-           */
-        } else {
-          // log.warn("thread {} marked as straggler - should be killed",
-          // thread.getName());
           threadsRemaining.add(thread.getName());
-        }
       }
     }
     if (threadsRemaining.size() > 0) {

From 43fff16c0eee391c33334ad16a880c5ee85ca538 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 08:16:41 -0800
Subject: [PATCH 051/131] corrected synchronization

---
 .../java/org/myrobotlab/service/Runtime.java  | 51 +++++++++----------
 1 file changed, 23 insertions(+), 28 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 49c27cff75..675123e962 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -1204,12 +1204,12 @@ public static <S extends ServiceInterface> S getService(String inName, StaticTyp
    *
    */
   static public String[] getServiceNames() {
-    Set<String> ret = registry.keySet();    
+    Set<String> ret = registry.keySet();
     String[] services = new String[ret.size()];
     if (ret.size() == 0) {
       return services;
     }
-    
+
     // if there are more than 0 services we need runtime
     // to filter to make sure they are "local"
     // and this requires a runtime service
@@ -1940,11 +1940,11 @@ public static boolean releaseService(String inName) {
     }
     return false;
   }
-  
-  
+
   /**
-   * Called after any subclassed releaseService has been called, this cleans
-   * up the registry and removes peers
+   * Called after any subclassed releaseService has been called, this cleans up
+   * the registry and removes peers
+   * 
    * @param inName
    * @return
    */
@@ -2004,7 +2004,6 @@ public static boolean releaseServiceInternal(String inName) {
     }
   }
 
-
   /**
    * Removes registration for a service. Removes the service from
    * {@link #typeToInterface} and {@link #interfaceToNames}.
@@ -2199,7 +2198,9 @@ static private void processRelease(boolean releaseRuntime) {
         if (runtime != null) {
           runtime.releaseService();
         }
-        runtime = null;
+        synchronized (INSTANCE_LOCK) {
+          runtime = null;
+        }
       } else {
         // put runtime in new registry
         Runtime.getInstance();
@@ -2830,26 +2831,20 @@ public Runtime(String n, String id) {
     // because you need to start with something ...
     config = new RuntimeConfig();
 
-    synchronized (INSTANCE_LOCK) {
-      if (runtime == null) {
-        // fist and only time....
-        runtime = this;
-        repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
-
-        /**
-         * This is used to run through all the possible services and determine
-         * if they have any missing dependencies. If they do not they become
-         * "installed". The installed flag makes the gui do a crossout when a
-         * service type is selected.
-         */
-        for (MetaData metaData : serviceData.getServiceTypes()) {
-          Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
-          if (deps.size() == 0) {
-            metaData.installed = true;
-          } else {
-            log.info("{} not installed", metaData.getSimpleName());
-          }
-        }
+    repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
+
+    /**
+     * This is used to run through all the possible services and determine if
+     * they have any missing dependencies. If they do not they become
+     * "installed". The installed flag makes the gui do a crossout when a
+     * service type is selected.
+     */
+    for (MetaData metaData : serviceData.getServiceTypes()) {
+      Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
+      if (deps.size() == 0) {
+        metaData.installed = true;
+      } else {
+        log.info("{} not installed", metaData.getSimpleName());
       }
     }
 

From 65542d07ac0532e27cca606a5422f68fd03c5bb1 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 08:40:00 -0800
Subject: [PATCH 052/131] corrected instance lock

---
 .../java/org/myrobotlab/service/Runtime.java  | 54 +++++++++----------
 1 file changed, 26 insertions(+), 28 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index e838cf6c52..675123e962 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -1204,12 +1204,12 @@ public static <S extends ServiceInterface> S getService(String inName, StaticTyp
    *
    */
   static public String[] getServiceNames() {
-    Set<String> ret = registry.keySet();    
+    Set<String> ret = registry.keySet();
     String[] services = new String[ret.size()];
     if (ret.size() == 0) {
       return services;
     }
-    
+
     // if there are more than 0 services we need runtime
     // to filter to make sure they are "local"
     // and this requires a runtime service
@@ -1940,11 +1940,11 @@ public static boolean releaseService(String inName) {
     }
     return false;
   }
-  
-  
+
   /**
-   * Called after any subclassed releaseService has been called, this cleans
-   * up the registry and removes peers
+   * Called after any subclassed releaseService has been called, this cleans up
+   * the registry and removes peers
+   * 
    * @param inName
    * @return
    */
@@ -2004,7 +2004,6 @@ public static boolean releaseServiceInternal(String inName) {
     }
   }
 
-
   /**
    * Removes registration for a service. Removes the service from
    * {@link #typeToInterface} and {@link #interfaceToNames}.
@@ -2199,7 +2198,9 @@ static private void processRelease(boolean releaseRuntime) {
         if (runtime != null) {
           runtime.releaseService();
         }
-        runtime = null;
+        synchronized (INSTANCE_LOCK) {
+          runtime = null;
+        }
       } else {
         // put runtime in new registry
         Runtime.getInstance();
@@ -2830,26 +2831,20 @@ public Runtime(String n, String id) {
     // because you need to start with something ...
     config = new RuntimeConfig();
 
-    synchronized (INSTANCE_LOCK) {
-      if (runtime == null) {
-        // fist and only time....
-        runtime = this;
-        repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
-
-        /**
-         * This is used to run through all the possible services and determine
-         * if they have any missing dependencies. If they do not they become
-         * "installed". The installed flag makes the gui do a crossout when a
-         * service type is selected.
-         */
-        for (MetaData metaData : serviceData.getServiceTypes()) {
-          Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
-          if (deps.size() == 0) {
-            metaData.installed = true;
-          } else {
-            log.info("{} not installed", metaData.getSimpleName());
-          }
-        }
+    repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
+
+    /**
+     * This is used to run through all the possible services and determine if
+     * they have any missing dependencies. If they do not they become
+     * "installed". The installed flag makes the gui do a crossout when a
+     * service type is selected.
+     */
+    for (MetaData metaData : serviceData.getServiceTypes()) {
+      Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
+      if (deps.size() == 0) {
+        metaData.installed = true;
+      } else {
+        log.info("{} not installed", metaData.getSimpleName());
       }
     }
 
@@ -3446,6 +3441,9 @@ public void releaseService() {
       runtime.stopService();
       runtime.stopInteractiveMode();
       runtime.getRepo().removeStatusPublishers();
+      if (cli != null) {
+        cli.stop();
+      }
       registry = new TreeMap<>();
     }
     synchronized (INSTANCE_LOCK) {

From 5182af01ea0fe9729c74cbc3634429e1608b2988 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 10:20:17 -0800
Subject: [PATCH 053/131] adjusted location of sleep wait

---
 src/test/java/org/myrobotlab/service/RandomTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 7c8add5923..dfde5042ee 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -62,8 +62,8 @@ public void testService() throws Exception {
     
     // disable all of a services random events
     random.disable("clock.startClock");
-    clock.stopClock();
     sleep(250);
+    clock.stopClock();
     assertTrue("clock should not be started 1", !clock.isClockRunning());
     
     // enable all of a service's random events

From 54f874bde713d48deddab53a00c779e13e6ff618 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 10:38:11 -0800
Subject: [PATCH 054/131] shifted sleep position in randomtest

---
 src/test/java/org/myrobotlab/service/RandomTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index dfde5042ee..18f4e1c789 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -74,8 +74,8 @@ public void testService() throws Exception {
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
-    clock.setInterval(9999);
     sleep(200);
+    clock.setInterval(9999);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
     assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
     assertTrue(String.format("random method 2 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);

From 8065ad4f3d8e6519aade45a52c1a8b14f5e49c13 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 15:52:53 -0800
Subject: [PATCH 055/131] config utils fix

---
 src/main/java/org/myrobotlab/config/ConfigUtils.java | 2 ++
 src/main/java/org/myrobotlab/service/Runtime.java    | 3 ---
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 19c256a8cf..69f850e602 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -60,6 +60,8 @@ static public RuntimeConfig loadRuntimeConfig(CmdOptions options) {
 
     if (startYml.enable) {
       configName = startYml.config;
+    } else {
+      configName = "default";
     }
 
     // start with default
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 675123e962..a768da57d1 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -910,9 +910,6 @@ public static Runtime getInstance() {
             // klunky
             Runtime.register(new Registration(runtime));
 
-            // assign, do not apply otherwise there will be
-            // a chicken-egg problem
-            runtime.config = c;
           }
 
           runtime.getRepo().addStatusPublisher(runtime);

From 942815729842add1ee160bb5a8b342cbd459ecfa Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 13 Feb 2024 18:24:37 -0800
Subject: [PATCH 056/131] clean default config

---
 src/test/java/org/myrobotlab/config/ConfigUtilsTest.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
index 5d15601b58..59caf6b201 100644
--- a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
+++ b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
@@ -3,9 +3,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
+import java.io.File;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.io.FileIO;
 import org.myrobotlab.service.Runtime;
 
 public class ConfigUtilsTest {
@@ -13,6 +16,8 @@ public class ConfigUtilsTest {
   @Before
   public void beforeTest() {
     Runtime.releaseAll(true, true);
+    // remove config
+    FileIO.rm("data/config/default");
   }
 
   @Test

From 63c00ecb2c817bce1901f561b2d6fdd4c9091ff8 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 14 Feb 2024 12:13:16 -0800
Subject: [PATCH 057/131] removed registering for new services from servo

---
 .../java/org/myrobotlab/service/DiyServo.java | 43 +++----------------
 .../java/org/myrobotlab/service/Servo.java    | 15 +------
 .../service/abstracts/AbstractServo.java      | 27 ------------
 3 files changed, 7 insertions(+), 78 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/DiyServo.java b/src/main/java/org/myrobotlab/service/DiyServo.java
index 2e125d8509..ae83c09118 100644
--- a/src/main/java/org/myrobotlab/service/DiyServo.java
+++ b/src/main/java/org/myrobotlab/service/DiyServo.java
@@ -28,7 +28,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
@@ -44,8 +43,6 @@
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.PinArrayControl;
 import org.myrobotlab.service.interfaces.PinListener;
-import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
-import org.myrobotlab.service.interfaces.ServoControl;
 import org.myrobotlab.service.interfaces.ServoEvent;
 import org.slf4j.Logger;
 
@@ -76,7 +73,7 @@
  *         TODO : move is not accurate ( 1° step seem not possible )
  */
 
-public class DiyServo extends AbstractServo<ServoConfig> implements PinListener, ServiceLifeCycleListener {
+public class DiyServo extends AbstractServo<ServoConfig> implements PinListener {
 
   double lastOutput = 0.0;
   /**
@@ -198,16 +195,6 @@ public DiyServo(String n, String id) {
     lastActivityTimeTs = System.currentTimeMillis();
   }
 
-  /*
-   * Update the list of PinArrayControls
-   */
-  @Override
-  public void onRegistered(Registration s) {
-    refreshPinArrayControls();
-    broadcastState();
-
-  }
-
   /**
    * Initiate the PID controller
    */
@@ -224,7 +211,7 @@ void initPid() {
     pid.setSetpoint(pidKey, setPoint);
     pid.startService();
   }
-  
+
   @Override
   public void startService() {
     super.startService();
@@ -232,7 +219,6 @@ public void startService() {
     motorControl = (MotorControl) startPeer("motor");
     initPid();
   }
-  
 
   /**
    * Equivalent to Arduino's Servo.detach() it de-energizes the servo
@@ -694,19 +680,17 @@ public static void main(String[] args) throws InterruptedException {
       // if (done) {
       // return;
       // }
-      
-      WebGui webgui = (WebGui)Runtime.create("webgui", "WebGui");
+
+      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
       webgui.autoStartBrowser(false);
       webgui.startService();
-      
+
       Runtime.start("diy", "DiyServo");
-      
-      
+
       boolean done = true;
       if (done) {
         return;
       }
-      
 
       String port = "COM4";
       Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino");
@@ -788,19 +772,4 @@ protected boolean processMove(Double newPos, boolean blocking, Long timeoutMs) {
     return false;
   }
 
-  @Override
-  public void onCreated(String name) {
-    log.info("created {}", name);
-  }
-
-  @Override
-  public void onStopped(String name) {
-    log.info("stopped {}", name);
-  }
-
-  @Override
-  public void onReleased(String name) {
-    log.info("released {}", name);
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/Servo.java b/src/main/java/org/myrobotlab/service/Servo.java
index 3e0b46ce95..07e251dec5 100644
--- a/src/main/java/org/myrobotlab/service/Servo.java
+++ b/src/main/java/org/myrobotlab/service/Servo.java
@@ -61,7 +61,7 @@
  * 
  */
 
-public class Servo extends AbstractServo<ServoConfig> implements ServiceLifeCycleListener {
+public class Servo extends AbstractServo<ServoConfig> {
 
   private static final long serialVersionUID = 1L;
 
@@ -337,17 +337,4 @@ public static void main(String[] args) throws InterruptedException {
     }
   }
 
-  @Override
-  public void onCreated(String name) {
-  }
-
-  @Override
-  public void onStopped(String name) {
-  }
-
-  @Override
-  public void onReleased(String name) {
-  }
-
-
 }
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
index 3378d5e56d..1376e78778 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
@@ -6,7 +6,6 @@
 import java.util.Set;
 
 import org.myrobotlab.codec.CodecUtils;
-import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
@@ -217,16 +216,6 @@ public abstract class AbstractServo<C extends ServoConfig> extends Service<C> im
 
   public AbstractServo(String n, String id) {
     super(n, id);
-    // this servo is interested in new services which support either
-    // ServoControllers or EncoderControl interfaces
-    // we subscribe to runtime here for new services
-    subscribeToRuntime("registered");
-    /*
-     * // new feature - // extracting the currentPos from serialized servo
-     * Double lastCurrentPos = null; try { lastCurrentPos = (Double)
-     * loadField("currentPos"); } catch (IOException e) {
-     * log.info("current pos cannot be found in saved file"); }
-     */
     // if no position could be loaded - set to rest
     // we have no "historical" info - assume we are @ rest
     targetPos = rest;
@@ -243,17 +232,6 @@ public AbstractServo(String n, String id) {
     }
   }
 
-  /**
-   * if a new service is added to the system refresh the controllers
-   */
-  @Deprecated /*
-               * lifecycle events not necessary for ui, probably should be
-               * pulled out
-               */
-  public void onStarted(String name) {
-    invoke("refreshControllers");
-  }
-
   /**
    * overloaded routing attach
    */
@@ -697,10 +675,6 @@ public void onEncoderData(EncoderData data) {
     }
   }
 
-  public void onRegistered(Registration s) {
-    refreshControllers();
-  }
-
   /**
    * Servo has the ability to act as an encoder if it is using TimeEncoder.
    * TimeEncoder will use Servo to publish a series of encoder events with
@@ -1096,7 +1070,6 @@ public ServoEvent publishServoStopped(String name, Double position) {
   @Override
   public void startService() {
     super.startService();
-    Runtime.getInstance().attachServiceLifeCycleListener(getName());
   }
 
   @Override

From aa65ad3bf4c6b46c3039318cf2e183554165c8e2 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 14 Feb 2024 12:57:59 -0800
Subject: [PATCH 058/131] webgui

---
 .../java/org/myrobotlab/service/WebGui.java   | 37 ++++-----
 .../org/myrobotlab/service/WebGuiTest.java    | 76 ++++++++++---------
 2 files changed, 55 insertions(+), 58 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 5e87cddb05..3e816726f1 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -34,7 +34,6 @@
 import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.MRLListener;
 import org.myrobotlab.framework.Message;
-import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
@@ -62,8 +61,7 @@
  * services are already APIs - perhaps a data API - same as service without the
  * message wrapper
  */
-public class WebGui extends Service<WebGuiConfig>
-    implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
+public class WebGui extends Service<WebGuiConfig> implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
 
   public static class LiveVideoStreamHandler implements Handler {
 
@@ -128,7 +126,7 @@ public Panel(String name, int x, int y, int z) {
    * needed to get the api key to select the appropriate api processor
    * 
    * @param uri
-   *            u
+   *          u
    * @return api key
    * 
    */
@@ -271,9 +269,9 @@ public boolean getAutoStartBrowser() {
    * String broadcast to specific client
    * 
    * @param uuid
-   *             u
+   *          u
    * @param str
-   *             s
+   *          s
    * 
    */
   public void broadcast(String uuid, String str) {
@@ -315,9 +313,7 @@ public Config.Builder getNettosphereConfig() {
         // cert.privateKey()).build();
 
         SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
-        SslContext context = SslContextBuilder
-            .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
-            .sslProvider(SslProvider.JDK)
+        SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK)
             .clientAuth(ClientAuth.NONE).build();
 
         configBuilder.sslContext(context);
@@ -496,8 +492,7 @@ public void handle(AtmosphereResource r) {
         } else if ((bodyData != null) && log.isDebugEnabled()) {
           logData = bodyData;
         }
-        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(),
-            request.getRequestURI(), logData, uuid);
+        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid);
       }
 
       // important persistent connections will have associated routes ...
@@ -575,8 +570,7 @@ public void handle(AtmosphereResource r) {
           }
 
           if (msg.containsHop(getId())) {
-            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender,
-                msg.name, msg.method);
+            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
             return;
           }
 
@@ -920,7 +914,7 @@ public void run() {
    * remotely control UI
    * 
    * @param panel
-   *              - the panel which has been moved or resized
+   *          - the panel which has been moved or resized
    */
   public void savePanel(Panel panel) {
     if (panel.name == null) {
@@ -1107,7 +1101,7 @@ public void releaseService() {
    * Default (false) is to use the CDN
    *
    * @param useLocalResources
-   *                          - true uses local resources fals uses cdn
+   *          - true uses local resources fals uses cdn
    */
   public void useLocalResources(boolean useLocalResources) {
     this.useLocalResources = useLocalResources;
@@ -1183,9 +1177,10 @@ public static void main(String[] args) {
 
     try {
 
-      Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui","intro", "Intro", "python", "Python" });
-      // Runtime.main(new String[] {});
-      // Runtime.main(new String[] { "--install" });
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
+      // "WebGui",
+      // "intro", "Intro", "python", "Python" });
+      Runtime.main(new String[] { "--install" });
 
       boolean done = true;
       if (done) {
@@ -1193,7 +1188,8 @@ public static void main(String[] args) {
       }
 
       // Platform.setVirtual(true);
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
+      // "WebGui",
       // "intro", "Intro", "python", "Python", "-c", "dev" });
       // Runtime.startConfig("dev");
 
@@ -1248,8 +1244,7 @@ public static void main(String[] args) {
       arduino.connect("/dev/ttyACM0");
 
       for (int i = 0; i < 1000; ++i) {
-        webgui.display(
-            "https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
+        webgui.display("https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
       }
 
       // Runtime.setLogLevel("ERROR");
diff --git a/src/test/java/org/myrobotlab/service/WebGuiTest.java b/src/test/java/org/myrobotlab/service/WebGuiTest.java
index 643485fc0c..02918dedba 100644
--- a/src/test/java/org/myrobotlab/service/WebGuiTest.java
+++ b/src/test/java/org/myrobotlab/service/WebGuiTest.java
@@ -5,7 +5,6 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
 import java.util.List;
 
 import org.junit.Before;
@@ -22,8 +21,8 @@
 public class WebGuiTest extends AbstractTest {
 
   public final static Logger log = LoggerFactory.getLogger(WebGui.class);
-  
-  // FIXME - DO A WEBSOCKET TEST 
+
+  // FIXME - DO A WEBSOCKET TEST
 
   @Before
   public void setUp() {
@@ -31,11 +30,11 @@ public void setUp() {
     webgui2.autoStartBrowser(false);
     webgui2.setPort(8889);
     webgui2.startService();
-    
-    Runtime.start("servoApiTest","Servo");
+
+    Runtime.start("servoApiTest", "Servo");
     Runtime.start("pythonApiTest", "Python");
     // need to wait for the OS to open the port
-    Service.sleep(3);
+    Service.sleep(200);
   }
 
   @Test
@@ -46,7 +45,7 @@ public void getTest() {
     String ret = new String(bytes);
     assertTrue(ret.contains("days"));
   }
-  
+
   @Test
   public void getTestWithParameter() throws UnsupportedEncodingException {
 
@@ -56,13 +55,12 @@ public void getTestWithParameter() throws UnsupportedEncodingException {
     assertTrue(ret.contains("true"));
   }
 
-
-// FIXME - ADD WHEN POST API IS WORKY
-// FIXME object non primitive (no string) post
+  // FIXME - ADD WHEN POST API IS WORKY
+  // FIXME object non primitive (no string) post
 
   @Test
   public void postTest() {
-    
+
     // 1st post - simple input - simple return
     String postBody = "[\"runtime\"]";
     byte[] bytes = Http.post("http://localhost:8889/api/service/runtime/getFullName", postBody);
@@ -70,7 +68,7 @@ public void postTest() {
     assertNotNull(bytes);
     String ret = new String(bytes);
     assertTrue(ret.contains("@"));
-    
+
     // second post - simple input - complex return
     postBody = "[\"runtime\"]";
     bytes = Http.post("http://localhost:8889/api/service/runtime/getService", postBody);
@@ -78,29 +76,31 @@ public void postTest() {
     assertNotNull(bytes);
     ret = new String(bytes);
     assertTrue(ret.contains("@"));
-    
-    
+
     // second post - simple input (including array of strings) - complex return
-    // FIXME uncomment when ready - callbacks are not possible through the rest api
-    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.toString exceeded
-    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.getFullName exceeded
-//    postBody = "[\"remoteId\", \"proxyName\", \"py:myService\",[\"org.myrobotlab.framework.interfaces.ServiceInterface\"]]";
-//    bytes = Http.post("http://localhost:8889/api/service/runtime/register", postBody);
-//    sleep(200);
-//    assertNotNull(bytes);
-//    ret = new String(bytes);
-//    assertTrue(ret.contains("remoteId"));
-    
-    
-    
+    // FIXME uncomment when ready - callbacks are not possible through the rest
+    // api
+    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for
+    // proxyName@remoteId.toString exceeded
+    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for
+    // proxyName@remoteId.getFullName exceeded
+    // postBody = "[\"remoteId\", \"proxyName\",
+    // \"py:myService\",[\"org.myrobotlab.framework.interfaces.ServiceInterface\"]]";
+    // bytes = Http.post("http://localhost:8889/api/service/runtime/register",
+    // postBody);
+    // sleep(200);
+    // assertNotNull(bytes);
+    // ret = new String(bytes);
+    // assertTrue(ret.contains("remoteId"));
+
     // post non primitive non string object
     MRLListener listener = new MRLListener("getRegistry", "runtime@webguittest", "onRegistry");
-    postBody = "[" + CodecUtils.toJson(listener) + "]";    
+    postBody = "[" + CodecUtils.toJson(listener) + "]";
     // postBody = "[\"runtime\"]";
     bytes = Http.post("http://localhost:8889/api/service/runtime/addListener", postBody);
     sleep(200);
     assertNotNull(bytes);
-    
+
     Runtime runtime = Runtime.getInstance();
     boolean found = false;
     List<MRLListener> check = runtime.getNotifyList("getRegistry");
@@ -108,9 +108,9 @@ public void postTest() {
       if (check.get(i).equals(listener)) {
         found = true;
       }
-    }    
+    }
     assertTrue("listener not found !", found);
-    
+
   }
 
   @Test
@@ -138,7 +138,7 @@ public void servoApiTest() {
 
   @Test
   public void urlEncodingTest() {
-    //exec("print \"hello\"")
+    // exec("print \"hello\"")
     byte[] bytes = Http.get("http://localhost:8889/api/service/pythonApiTest/exec/%22print+%5C%22hello%5C%22%22");
     String ret = new String(bytes);
     assertEquals("true", ret);
@@ -147,16 +147,19 @@ public void urlEncodingTest() {
   @Test
   public void sendBlockingTest() throws InterruptedException, TimeoutException {
     String retVal = "retVal";
-    // Put directly in blocking list because sendBlocking() won't use it for local services
+    // Put directly in blocking list because sendBlocking() won't use it for
+    // local
+    // services
     Runtime.getInstance().getInbox().blockingList.put("runtime.onBlocking", new Object[1]);
     Object[] blockingListRet = Runtime.getInstance().getInbox().blockingList.get("runtime.onBlocking");
 
     // Delay in a new thread so we can get our wait() call in first
     new Thread(() -> {
       try {
-        Thread.sleep(50);
-      } catch (InterruptedException ignored) {}
-      Http.post("http://localhost:8889/api/service/runtime/onBlocking", "[\""+retVal+"\"]");
+        Thread.sleep(100);
+      } catch (InterruptedException ignored) {
+      }
+      Http.post("http://localhost:8889/api/service/runtime/onBlocking", "[\"" + retVal + "\"]");
     }).start();
 
     long timeout = 1000;
@@ -170,6 +173,5 @@ public void sendBlockingTest() throws InterruptedException, TimeoutException {
 
     assertEquals(retVal, blockingListRet[0]);
   }
-  
-  
+
 }

From 33a1be5181a9ba3fdb82ad8750e00db32b8ee99e Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 14 Feb 2024 13:00:00 -0800
Subject: [PATCH 059/131] adding log service to scripts

---
 myrobotlab.bat | 2 +-
 myrobotlab.sh  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/myrobotlab.bat b/myrobotlab.bat
index b8515ca03d..a6ce3e30ea 100644
--- a/myrobotlab.bat
+++ b/myrobotlab.bat
@@ -28,6 +28,6 @@ IF NOT "%*"=="" (
         "%JAVA%" %JAVA_OPTIONS% -cp %CLASSPATH% org.myrobotlab.service.Runtime --install --log-file myrobotlab-install.log
     )
 
-    "%JAVA%" %JAVA_OPTIONS% -cp %CLASSPATH% org.myrobotlab.service.Runtime --log-level info -s webgui WebGui intro Intro python Python
+    "%JAVA%" %JAVA_OPTIONS% -cp %CLASSPATH% org.myrobotlab.service.Runtime --log-level info -s log Log webgui WebGui intro Intro python Python
 
 )
\ No newline at end of file
diff --git a/myrobotlab.sh b/myrobotlab.sh
index 9aa528ea51..4cc45eb085 100755
--- a/myrobotlab.sh
+++ b/myrobotlab.sh
@@ -62,6 +62,6 @@ else
     "${JAVA}" ${JAVA_OPTIONS} -cp ${CLASSPATH} org.myrobotlab.service.Runtime --install --log-file myrobotlab-install.log
 fi
 
-"${JAVA}" ${JAVA_OPTIONS} -cp ${CLASSPATH} org.myrobotlab.service.Runtime --log-level info -s webgui WebGui intro Intro python Python
+"${JAVA}" ${JAVA_OPTIONS} -cp ${CLASSPATH} org.myrobotlab.service.Runtime --log-level info -s log Log webgui WebGui intro Intro python Python
 
 echo $# $@
\ No newline at end of file

From 77eac6ecfa500bbb5f0ecbd5a6cb228d027657fe Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 14 Feb 2024 13:16:27 -0800
Subject: [PATCH 060/131] simplifying randomtest

---
 .../org/myrobotlab/service/RandomTest.java    | 52 ++++++++-----------
 1 file changed, 23 insertions(+), 29 deletions(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 18f4e1c789..f089f2e453 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -7,31 +7,24 @@
 import java.util.Map;
 
 import org.junit.Before;
-import org.myrobotlab.framework.Service;
+import org.junit.Test;
 import org.myrobotlab.service.Random.RandomMessage;
+import org.myrobotlab.test.AbstractTest;
 
-public class RandomTest extends AbstractServiceTest {
+public class RandomTest extends AbstractTest {
 
-  @Override /*
-             * FIXME - this assumes a single service is in the test - which
-             * rarely happens - seems not useful and silly
-             */
-  public Service createService() throws Exception {
-    return (Service) Runtime.start("randomTest", "Random");
-  }
-  
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-      // clean our config directory
+    // clean our config directory
     // Runtime.removeConfig("RandomTest");
     // set our config
     Runtime.setConfig("RandomTest");
+    Runtime.start("randomTest", "Random");
   }
-  
 
-  @Override
+  @Test
   public void testService() throws Exception {
     Clock clock = (Clock) Runtime.start("clock", "Clock");
     Random random = (Random) Runtime.start("randomTest", "Random");
@@ -46,31 +39,31 @@ public void testService() throws Exception {
     sleep(1000);
 
     assertTrue("should have method", random.getKeySet().contains("clock.setInterval"));
-    
+
     assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 1 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
-    
+    assertTrue(String.format("random method 1 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
+
     random.remove("clock.setInterval");
-    
+
     assertTrue("should not have method", !random.getKeySet().contains("clock.setInterval"));
 
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.addRandom(0, 200, "clock", "startClock");
-    
+
     sleep(500);
     assertTrue("clock should be started 1", clock.isClockRunning());
-    
+
     // disable all of a services random events
     random.disable("clock.startClock");
     sleep(250);
     clock.stopClock();
     assertTrue("clock should not be started 1", !clock.isClockRunning());
-    
+
     // enable all of a service's random events
     random.enable("clock.startClock");
     sleep(250);
     assertTrue("clock should be started 2", clock.isClockRunning());
-    
+
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
@@ -78,30 +71,31 @@ public void testService() throws Exception {
     clock.setInterval(9999);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
     assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 2 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 2 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
 
     // disable all
     random.disable();
     sleep(200);
     clock.setInterval(9999);
-    assertTrue("clock should not be started 4", !clock.isClockRunning());   
-    assertEquals(9999, (long)clock.getInterval());
+    assertTrue("clock should not be started 4", !clock.isClockRunning());
+    assertEquals(9999, (long) clock.getInterval());
 
-    // re-enable all that were previously enabled but not explicitly disabled ones
+    // re-enable all that were previously enabled but not explicitly disabled
+    // ones
     random.enable();
     sleep(1000);
     assertTrue("clock should not be started 5", !clock.isClockRunning());
     assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 3 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
 
     clock.stopClock();
     random.purge();
-        
+
     Map<String, RandomMessage> events = random.getRandomEvents();
     assertTrue(events.size() == 0);
-    
+
     random.addRandom("named task", 200, 500, "clock", "setInterval", 100, 1000, 10);
-    
+
     clock.releaseService();
     random.releaseService();
 

From 1d9cb9050cf488967e10246be5f7b2470d98ec90 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 14 Feb 2024 16:58:11 -0800
Subject: [PATCH 061/131] fixes and updates

---
 .../service/AdafruitMotorHat4Pi.java          |  4 +-
 .../java/org/myrobotlab/service/InMoov2.java  | 71 ------------------
 .../java/org/myrobotlab/service/RoboClaw.java |  4 +-
 .../java/org/myrobotlab/service/Servo.java    | 72 -------------------
 .../java/org/myrobotlab/service/WebGui.java   |  6 +-
 .../abstracts/AbstractMotorController.java    |  4 +-
 .../config/AbstractMotorControllerConfig.java |  5 ++
 .../service/config/InMoov2Config.java         |  7 +-
 .../service/config/SabertoothConfig.java      |  2 +-
 9 files changed, 19 insertions(+), 156 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java

diff --git a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
index a089498d4f..5897630ef8 100644
--- a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
+++ b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
@@ -19,7 +19,7 @@
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.abstracts.AbstractMotorController;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
 import org.myrobotlab.service.interfaces.MotorControl;
@@ -34,7 +34,7 @@
  *         https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi/overview
  */
 
-public class AdafruitMotorHat4Pi extends AbstractMotorController<MotorConfig> implements I2CControl {
+public class AdafruitMotorHat4Pi extends AbstractMotorController<AbstractMotorControllerConfig> implements I2CControl {
 
   /** version of the library */
   static public final String VERSION = "0.9";
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index e6ad6326ab..40ebda2e26 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -188,77 +188,6 @@ public static void main(String[] args) {
         return;
       }
 
-      OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {
-      });
-      ocvConfig.flip = true;
-      i01.setPeerConfigValue("opencv", "flip", true);
-      // i01.savePeerConfig("", null);
-
-      // Runtime.startConfig("default");
-
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
-      // "WebGui",
-      // "intro", "Intro", "python", "Python" });
-
-      Runtime.start("python", "Python");
-      // Runtime.start("ros", "Ros");
-      Runtime.start("intro", "Intro");
-      // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-      // i01.startPeer("simulator");
-      // Runtime.startConfig("i01-05");
-      // Runtime.startConfig("pir-01");
-
-      // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly");
-      // i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-
-      // polly.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-      // i01.startPeer("mouth");
-      // i01.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-
-      Runtime.start("python", "Python");
-
-      // i01.startSimulator();
-      Plan plan = Runtime.load("webgui", "WebGui");
-      // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui");
-      // webgui.autoStartBrowser = false;
-      Runtime.startConfig("webgui");
-      Runtime.start("webgui", "WebGui");
-
-      Random random = (Random) Runtime.start("random", "Random");
-
-      random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-      random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-
-      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          130.0, 175.0);
-      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          5.0, 40.0);
-
-      random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
-      random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
-
-      random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0);
-      random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0);
-
-      random.save();
-
-      // i01.startChatBot();
-      //
-      // i01.startAll("COM3", "COM4");
-      Runtime.start("python", "Python");
-
     } catch (Exception e) {
       log.error("main threw", e);
     }
diff --git a/src/main/java/org/myrobotlab/service/RoboClaw.java b/src/main/java/org/myrobotlab/service/RoboClaw.java
index 83203b99f8..8d22f7ff27 100644
--- a/src/main/java/org/myrobotlab/service/RoboClaw.java
+++ b/src/main/java/org/myrobotlab/service/RoboClaw.java
@@ -16,7 +16,7 @@
 import org.myrobotlab.serial.CRC;
 import org.myrobotlab.service.Pid.PidData;
 import org.myrobotlab.service.abstracts.AbstractMotorController;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.MotorController;
 import org.myrobotlab.service.interfaces.PortConnector;
@@ -55,7 +55,7 @@
  *         this value IS correct
  * 
  */
-public class RoboClaw extends AbstractMotorController<MotorConfig> implements EncoderPublisher, PortConnector, MotorController, SerialDataListener {
+public class RoboClaw extends AbstractMotorController<AbstractMotorControllerConfig> implements EncoderPublisher, PortConnector, MotorController, SerialDataListener {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Servo.java b/src/main/java/org/myrobotlab/service/Servo.java
index 3e0b46ce95..798bff3afb 100644
--- a/src/main/java/org/myrobotlab/service/Servo.java
+++ b/src/main/java/org/myrobotlab/service/Servo.java
@@ -259,78 +259,6 @@ public static void main(String[] args) throws InterruptedException {
         return;
       }
 
-      // runtime.save();
-
-      /*
-       * mega.save(); tilt.save(); pan.save();
-       * 
-       * mega.load(); tilt.load(); pan.load();
-       */
-
-      // TODO - attach before and after connect..
-
-      // mega.setBoardMega();
-
-      // log.info("servo pos {}", tilt.getCurrentInputPos());
-      //
-      // // double pos = 170;
-      // // servo03.setPosition(pos);
-      //
-      // double min = 3;
-      // double max = 170;
-      // double speed = 60; // degree/s
-      //
-      // mega.attach(tilt);
-      // // mega.attach(servo03,3);
-      //
-      // for (int i = 0; i < 100; ++i) {
-      // tilt.moveTo(20.0);
-      // }
-      //
-      // tilt.sweep(min, max, speed);
-
-      /*
-       * Servo servo04 = (Servo) Runtime.start("servo04", "Servo"); Servo
-       * servo05 = (Servo) Runtime.start("servo05", "Servo"); Servo servo06 =
-       * (Servo) Runtime.start("servo06", "Servo"); Servo servo07 = (Servo)
-       * Runtime.start("servo07", "Servo"); Servo servo08 = (Servo)
-       * Runtime.start("servo08", "Servo"); Servo servo09 = (Servo)
-       * Runtime.start("servo09", "Servo"); Servo servo10 = (Servo)
-       * Runtime.start("servo10", "Servo"); Servo servo11 = (Servo)
-       * Runtime.start("servo11", "Servo"); Servo servo12 = (Servo)
-       * Runtime.start("servo12", "Servo");
-       */
-      // Servo servo13 = (Servo) Runtime.start("servo13", "Servo");
-
-      // servo03.attach(mega, 8, 38.0);
-      /*
-       * servo04.attach(mega, 4, 38.0); servo05.attach(mega, 5, 38.0);
-       * servo06.attach(mega, 6, 38.0); servo07.attach(mega, 7, 38.0);
-       * servo08.attach(mega, 8, 38.0); servo09.attach(mega, 9, 38.0);
-       * servo10.attach(mega, 10, 38.0); servo11.attach(mega, 11, 38.0);
-       * servo12.attach(mega, 12, 38.0);
-       */
-
-      // TestCatcher catcher = (TestCatcher)Runtime.start("catcher",
-      // "TestCatcher");
-      // servo03.attach((ServoEventListener)catcher);
-
-      // servo.setPin(12);
-
-      /*
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       */
-
-      // servo.sweepDelay = 3;
-      // servo.save();
-      // servo.load();
-      // servo.save();
-      // log.info("sweepDely {}", servo.sweepDelay);
 
     } catch (Exception e) {
       log.error("main threw", e);
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 3e816726f1..f50cdc238a 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -1177,10 +1177,8 @@ public static void main(String[] args) {
 
     try {
 
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
-      // "WebGui",
-      // "intro", "Intro", "python", "Python" });
-      Runtime.main(new String[] { "--install" });
+      Runtime.main(new String[] { "--log-level", "info", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
+      // Runtime.main(new String[] { "--install" });
 
       boolean done = true;
       if (done) {
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
index 5f01ebc74f..94380c9711 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
@@ -8,11 +8,11 @@
 import org.myrobotlab.math.MapperLinear;
 import org.myrobotlab.math.interfaces.Mapper;
 import org.myrobotlab.service.Runtime;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.MotorController;
 
-public abstract class AbstractMotorController<C extends MotorConfig> extends Service<C> implements MotorController {
+public abstract class AbstractMotorController<C extends AbstractMotorControllerConfig> extends Service<C> implements MotorController {
 
   /**
    * currently attached motors to this controller
diff --git a/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java b/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java
new file mode 100644
index 0000000000..030a1ee4f0
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java
@@ -0,0 +1,5 @@
+package org.myrobotlab.service.config;
+
+public class AbstractMotorControllerConfig extends ServiceConfig {
+    // Add your configuration here
+}
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index a050223baa..99d40fa8ed 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -281,8 +281,7 @@ public Plan getDefault(Plan plan, String name) {
     // setup name references to different services
     MarySpeechConfig mouth = (MarySpeechConfig) plan.get(getPeerName("mouth"));
     mouth.voice = "Mark";
-    mouth.speechRecognizers = new String[] { name + ".ear" };
-
+    
     // == Peer - ear =============================
     // setup name references to different services
     WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear"));
@@ -549,6 +548,10 @@ public Plan getDefault(Plan plan, String name) {
 
     // Needs upcoming pr
     fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
+    
+    // peer --to--> peer
+    mouth.listeners.add(new Listener("publishStartSpeaking", getPeerName("ear")));
+    mouth.listeners.add(new Listener("publishEndSpeaking", getPeerName("ear")));
 
     return plan;
   }
diff --git a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
index 42db50689b..44dc5a0c5b 100644
--- a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
@@ -2,7 +2,7 @@
 
 import org.myrobotlab.framework.Plan;
 
-public class SabertoothConfig extends MotorConfig {
+public class SabertoothConfig extends AbstractMotorControllerConfig {
 
   public String port;
   public boolean connect = false;

From 402a527b8898dee23b3555835c593217e4717f4f Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 16 Feb 2024 10:05:36 -0800
Subject: [PATCH 062/131] servo.setMaxSpeed

---
 .../service/FiniteStateMachine.java           | 44 ++++++++-----------
 .../java/org/myrobotlab/service/InMoov2.java  |  5 ++-
 .../service/abstracts/AbstractServo.java      |  6 +++
 .../service/config/InMoov2Config.java         |  1 +
 .../service/interfaces/ServoControl.java      |  8 +++-
 .../org/myrobotlab/service/ServoTest.java     |  2 +-
 6 files changed, 37 insertions(+), 29 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
index 32c04bf29e..e998711e26 100644
--- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
+++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
@@ -2,16 +2,11 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 
-import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.Service;
-import org.myrobotlab.framework.interfaces.MessageListener;
-import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.generics.SlidingWindowList;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
@@ -61,7 +56,7 @@ public class Tuple {
     public Transition transition;
     public StateTransition stateTransition;
   }
-  
+
   public class StateChange {
     /**
      * timestamp
@@ -72,7 +67,7 @@ public class StateChange {
      * current new state
      */
     public String state;
-    
+
     /**
      * event which activated new state
      */
@@ -82,13 +77,12 @@ public class StateChange {
      * source of event
      */
     public String src = getName();
-    
-    
+
     public StateChange(String current, String event) {
       this.state = current;
       this.event = event;
     }
-    
+
     public String toString() {
       return String.format("%s --%s--> %s", last, event, state);
     }
@@ -221,25 +215,25 @@ public String firedEvent(String event) {
   }
 
   /**
-   * gets the current state of this state machine
+   * get the previous state of this state machine
    * 
    * @return
    */
-  public String getCurrent() {
-    if (current != null) {
-      return current.getName();
+  public String getLast() {
+    if (last != null) {
+      return last.getName();
     }
     return null;
   }
 
   /**
-   * get the previous state of this state machine
+   * gets the current state of this state machine
    * 
    * @return
    */
-  public String getLast() {
-    if (last != null) {
-      return last.getName();
+  public String getState() {
+    if (current != null) {
+      return current.getName();
     }
     return null;
   }
@@ -250,7 +244,7 @@ public List<Transition> getTransitions() {
   }
 
   /**
-   * Publishes state change (current, last and event) 
+   * Publishes state change (current, last and event)
    * 
    * @param stateChange
    * @return
@@ -263,7 +257,7 @@ public StateChange publishStateChange(StateChange stateChange) {
   @Override
   public FiniteStateMachineConfig getConfig() {
     super.getConfig();
-    config.current = getCurrent();
+    config.current = getState();
     return config;
   }
 
@@ -361,15 +355,15 @@ public static void main(String[] args) {
 
       // fsm.subscribe("fsm", "publishState");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.setCurrent("neutral");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.fire("ill-event");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.fire("ill-event");
       fsm.fire("ill-event");
@@ -387,7 +381,7 @@ public static void main(String[] args) {
 
       // fsm.removeScheduledEvents();
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
     } catch (Exception e) {
       log.error("main threw", e);
@@ -419,7 +413,7 @@ public String getPreviousState() {
       return history.get(history.size() - 2).state;
     }
   }
-  
+
   @Override
   public void startService() {
     super.startService();
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 40ebda2e26..36d8c07ff4 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -856,7 +856,7 @@ public String getState() {
     if (fsm == null) {
       return null;
     }
-    return fsm.getCurrent();
+    return fsm.getState();
   }
 
   /**
@@ -1553,7 +1553,8 @@ public Heartbeat publishHeartbeat() {
 
       if (System.currentTimeMillis() > stateLastIdleTime + (config.stateIdleInterval * 1000)) {
         // idle event to be handled with the processor
-        processMessage("onIdle");
+        // processMessage("onIdle");
+        fire("idle");
         stateLastIdleTime = System.currentTimeMillis();
       }
 
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
index 1376e78778..780f875741 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
@@ -434,6 +434,12 @@ public void enable() {
   public void fullSpeed() {
     setSpeed((Double) null);
   }
+  
+  @Override
+  public void setMaxSpeed() {
+    setSpeed((Double) null);
+  }
+
 
   @Override
   public boolean isAutoDisable() {
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 230f787aa9..819f84e31e 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -380,6 +380,7 @@ public Plan getDefault(Plan plan, String name) {
     fsm.transitions.add(new Transition("sleep", "power_down", "power_down"));
     fsm.transitions.add(new Transition("idle", "power_down", "power_down"));
     fsm.transitions.add(new Transition("wake", "setup", "setup"));
+    fsm.transitions.add(new Transition("wake", "idle", "idle"));
     fsm.transitions.add(new Transition("idle", "setup", "setup"));
     // power_down to shutdown
     // fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished",
diff --git a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
index 23a9ee9629..379154cd6e 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
@@ -404,6 +404,12 @@ public interface ServoControl extends AbsolutePositionControl, EncoderListener,
   /**
    * disable speed control and move the servos at full speed.
    */
+  @Deprecated
   void fullSpeed();
-
+  
+  /**
+   * 
+   */
+  void setMaxSpeed();
+  
 }
diff --git a/src/test/java/org/myrobotlab/service/ServoTest.java b/src/test/java/org/myrobotlab/service/ServoTest.java
index 8d96052e2e..819c3718d6 100644
--- a/src/test/java/org/myrobotlab/service/ServoTest.java
+++ b/src/test/java/org/myrobotlab/service/ServoTest.java
@@ -73,7 +73,7 @@ public void autoDisableAfterAttach() {
   @Test
   public void disabledMove() throws Exception {
     // take off speed control
-    servo.fullSpeed();
+    servo.setMaxSpeed();
     servo.moveTo(0.0);
     servo.setInverted(false);
     Service.sleep(1000);

From 8601157c27b3c97c3a065385a8b91142c85eeb6d Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 16 Feb 2024 10:37:01 -0800
Subject: [PATCH 063/131] ignoring servo.setSpeed(speed <0)

---
 .../java/org/myrobotlab/service/abstracts/AbstractServo.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
index 780f875741..7eee94dc5a 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
@@ -898,6 +898,11 @@ public void setSpeed(Double degreesPerSecond) {
     // speed = maxSpeed;
     // log.info("Trying to set speed to a value greater than max speed");
     // }
+    
+    if (degreesPerSecond != null && degreesPerSecond < 0) {
+      warn("setting speed to negative value %d ignoring", degreesPerSecond);
+      return;
+    }
 
     speed = degreesPerSecond;
 

From 11fefcae4e201b479cbd3770b8b07fb925d2379a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 16 Feb 2024 10:59:51 -0800
Subject: [PATCH 064/131] synching cpython javacpp and javacv versions to 15.8

---
 src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
index 1b8f134beb..73d284cdb6 100644
--- a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
@@ -16,7 +16,7 @@ public OpenCVMeta() {
 
     addDescription("OpenCV (computer vision) service wrapping many of the functions and filters of OpenCV");
     addCategory("video", "vision", "sensors");
-    String javaCvVersion = "1.5.7";
+    String javaCvVersion = "1.5.8";
     // addDependency("org.bytedeco", "javacv", javaCvVersion);
     addDependency("org.bytedeco", "javacv-platform", javaCvVersion);
     addDependency("org.bytedeco", "javacpp", javaCvVersion);

From db9ccdd1499cae0350a31c13ed6762df91ba57f0 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 06:46:55 -0800
Subject: [PATCH 065/131] removal of programab botdir

---
 .../java/org/myrobotlab/service/InMoov2.java  | 22 +++++++------------
 .../org/myrobotlab/service/ProgramAB.java     | 12 ----------
 .../service/config/InMoov2Config.java         |  1 -
 .../service/config/ProgramABConfig.java       |  5 -----
 4 files changed, 8 insertions(+), 32 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 36d8c07ff4..8aac57a03e 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -2318,26 +2318,20 @@ public void stopNeopixelAnimation() {
   }
 
   public void systemCheck() {
-    log.error("systemCheck()");
-    Runtime runtime = Runtime.getInstance();
-    int servoCount = 0;
-    int servoAttachedCount = 0;
+    Platform platform = Runtime.getPlatform();
+    int servoCount = 0;    
     for (ServiceInterface si : Runtime.getServices()) {
       if (si.getClass().getSimpleName().equals("Servo")) {
         servoCount++;
-        if (((Servo) si).getController() != null) {
-          servoAttachedCount++;
-        }
       }
     }
 
-    setPredicate("systemServoCount", servoCount);
-    setPredicate("systemAttachedServoCount", servoAttachedCount);
-    setPredicate("systemFreeMemory", Runtime.getFreeMemory());
-    Platform platform = Runtime.getPlatform();
-    setPredicate("system version", platform.getVersion());
-    // ERROR buffer !!!
-    systemEvent("SYSTEMCHECKFINISHED"); // wtf is this?
+    setPredicate("system_uptime", Runtime.getUptime());
+    setPredicate("system_servo_count", servoCount);
+    setPredicate("system_free_memory", Runtime.getFreeMemory());
+    setPredicate("system_version", platform.getVersion());
+    setPredicate("system_errors", errors.size());
+    
   }
 
   public String systemEvent(String eventMsg) {
diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java
index 804115b67e..63dc903174 100644
--- a/src/main/java/org/myrobotlab/service/ProgramAB.java
+++ b/src/main/java/org/myrobotlab/service/ProgramAB.java
@@ -943,9 +943,6 @@ public void startService() {
     logging.setLevel("org.alicebot.ab.MagicBooleans", "DEBUG");
     logging.setLevel("class org.myrobotlab.programab.MrlSraixHandler", "DEBUG");
     logPublisher.start();
-
-    scanForBots(getResourceDir());
-
   }
 
   @Override /* FIXME - just do this once in abstract */
@@ -1104,15 +1101,6 @@ public ProgramABConfig apply(ProgramABConfig c) {
       }
     }
 
-    if (c.botDir == null) {
-      c.botDir = getResourceDir();
-    }
-
-    List<File> botsFromScanning = scanForBots(c.botDir);
-    for (File file : botsFromScanning) {
-      addBotPath(file.getAbsolutePath());
-    }
-
     if (c.currentUserName != null) {
       setCurrentUserName(c.currentUserName);
     }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 819f84e31e..889e91eec9 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -243,7 +243,6 @@ public Plan getDefault(Plan plan, String name) {
     
     
     ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot"));
-    chatBot.botDir = "resource/ProgramAB";
 
     chatBot.bots.add("resource/ProgramAB/Alice");
     chatBot.bots.add("resource/ProgramAB/Dr.Who");
diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
index ce9ae14033..0cd4dbc839 100644
--- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
@@ -10,11 +10,6 @@ public class ProgramABConfig extends ServiceConfig {
   @Deprecated /* unused text filters */
   public String[] textFilters;
   
-  /**
-   * a directory ProgramAB will scan for new bots
-   */
-  public String botDir;
-
   /**
    * explicit bot directories
    */

From b7c7a165c0cf5cf6b99015c30bfba893605d84ec Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:00:06 -0800
Subject: [PATCH 066/131] more non inmoov2 updates

---
 .../org/myrobotlab/config/ConfigUtils.java    |   2 +
 .../org/myrobotlab/framework/repo/Repo.java   |   6 +-
 .../service/AdafruitMotorHat4Pi.java          |   4 +-
 .../java/org/myrobotlab/service/Arduino.java  | 154 ++----------------
 .../java/org/myrobotlab/service/DiyServo.java |  43 +----
 .../service/FiniteStateMachine.java           |  44 +++--
 .../java/org/myrobotlab/service/RoboClaw.java |   4 +-
 .../java/org/myrobotlab/service/Runtime.java  |  23 +--
 .../java/org/myrobotlab/service/Servo.java    |  87 +---------
 .../java/org/myrobotlab/service/WebGui.java   |  33 ++--
 .../abstracts/AbstractMotorController.java    |   4 +-
 .../service/abstracts/AbstractServo.java      |  38 ++---
 .../config/AbstractMotorControllerConfig.java |   5 +
 .../service/config/ArduinoConfig.java         |   9 +
 .../service/config/InMoov2Config.java         |  48 ++++--
 .../service/config/ProgramABConfig.java       |   5 -
 .../service/config/SabertoothConfig.java      |   2 +-
 .../config/UltrasonicSensorConfig.java        |   2 +
 .../service/interfaces/ServoControl.java      |   9 +-
 .../myrobotlab/service/meta/OpenCVMeta.java   |   2 +-
 .../myrobotlab/config/ConfigUtilsTest.java    |   3 +
 .../myrobotlab/framework/BlockingTest.java    |   4 +-
 .../myrobotlab/framework/repo/RepoTest.java   |   4 +
 .../org/myrobotlab/service/ArduinoTest.java   |   1 -
 .../org/myrobotlab/service/RandomTest.java    |  56 +++----
 .../service/RuntimeProcessTest.java           |   5 +-
 .../org/myrobotlab/service/ServoTest.java     |   2 +-
 .../service/VirtualArduinoTest.java           |   4 +
 .../org/myrobotlab/service/WebGuiTest.java    |  76 ++++-----
 .../org/myrobotlab/test/AbstractTest.java     | 149 ++++++-----------
 30 files changed, 273 insertions(+), 555 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 19c256a8cf..69f850e602 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -60,6 +60,8 @@ static public RuntimeConfig loadRuntimeConfig(CmdOptions options) {
 
     if (startYml.enable) {
       configName = startYml.config;
+    } else {
+      configName = "default";
     }
 
     // start with default
diff --git a/src/main/java/org/myrobotlab/framework/repo/Repo.java b/src/main/java/org/myrobotlab/framework/repo/Repo.java
index 9262834da1..d8b6863c16 100644
--- a/src/main/java/org/myrobotlab/framework/repo/Repo.java
+++ b/src/main/java/org/myrobotlab/framework/repo/Repo.java
@@ -324,10 +324,10 @@ public Set<ServiceDependency> getUnfulfilledDependencies(String[] types) {
           }
         }
       }
-      
+
       // Plan plan = ServiceConfig.getDefault(type.toLowerCase(), type);
       ServiceConfig sc = ServiceConfig.getDefaultServiceConfig(type);
-      
+
       Map<String, Peer> peers = sc.getPeers();
       if (peers != null) {
         for (String key : peers.keySet()) {
@@ -496,7 +496,7 @@ public void load() {
         }
 
       } else {
-        log.info("{} not found", getRepoPath());
+        log.info("{} not found", f.getAbsolutePath());
       }
 
     } catch (Exception e) {
diff --git a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
index a089498d4f..5897630ef8 100644
--- a/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
+++ b/src/main/java/org/myrobotlab/service/AdafruitMotorHat4Pi.java
@@ -19,7 +19,7 @@
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.abstracts.AbstractMotorController;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.I2CControl;
 import org.myrobotlab.service.interfaces.I2CController;
 import org.myrobotlab.service.interfaces.MotorControl;
@@ -34,7 +34,7 @@
  *         https://learn.adafruit.com/adafruit-dc-and-stepper-motor-hat-for-raspberry-pi/overview
  */
 
-public class AdafruitMotorHat4Pi extends AbstractMotorController<MotorConfig> implements I2CControl {
+public class AdafruitMotorHat4Pi extends AbstractMotorController<AbstractMotorControllerConfig> implements I2CControl {
 
   /** version of the library */
   static public final String VERSION = "0.9";
diff --git a/src/main/java/org/myrobotlab/service/Arduino.java b/src/main/java/org/myrobotlab/service/Arduino.java
index 1f467cf8d8..cfc1be7b6d 100644
--- a/src/main/java/org/myrobotlab/service/Arduino.java
+++ b/src/main/java/org/myrobotlab/service/Arduino.java
@@ -171,7 +171,6 @@ public static class I2CDeviceMap {
 
   transient Mapper motorPowerMapper = new MapperLinear(-1.0, 1.0, -255.0, 255.0);
 
-  // make final - if not "connected" log error but don't allow Arduino NPEs
   public final transient Msg msg = new Msg(this, null);
 
   Integer nextDeviceId = 0;
@@ -191,10 +190,6 @@ public static class I2CDeviceMap {
 
   private volatile boolean syncInProgress = false;
 
-  /**
-   * the port the user attempted to connect to
-   */
-  String port;
 
   public Arduino(String n, String id) {
     super(n, id);
@@ -552,6 +547,7 @@ public VirtualArduino getVirtual() {
    */
   @Override
   public void connect(String port, int rate, int databits, int stopbits, int parity) {
+    config.connect = true;
     connecting = true;
     if (port == null) {
       warn("%s attempted to connect with a null port", getName());
@@ -563,7 +559,7 @@ public void connect(String port, int rate, int databits, int stopbits, int parit
     serial.addByteListener(this);
 
     // test to see if we've been started. the serial might be null
-    this.port = port;
+    config.port = port;
 
     try {
 
@@ -811,6 +807,7 @@ public void disablePins() {
 
   @Override
   public void disconnect() {
+    config.connect = false;
     // FIXED - all don in 'onDisconnect()'
     // enableBoardInfo(false);
     // boardInfo is not valid after disconnect
@@ -2233,7 +2230,7 @@ public Map<String, DeviceMapping> getDeviceList() {
 
   @Override
   public void ackTimeout() {
-    log.warn("{} Ack Timeout seen.  TODO: consider resetting the com port {}, reconnecting and re syncing all devices.", getName(), port);
+    log.warn("{} Ack Timeout seen.  TODO: consider resetting the com port {}, reconnecting and re syncing all devices.", getName(), config.port);
   }
 
   @Override
@@ -2325,34 +2322,13 @@ public void neoPixelClear(String neopixel) {
     msg.neoPixelClear(getDeviceId(neopixel));
   }
 
-  @Override
-  public ArduinoConfig getConfig() {
-    super.getConfig();
-
-    // FIXME "port" shouldn't exist only config.port !
-    config.port = port;
-    config.connect = isConnected();
-
-    return config;
-  }
-
   @Override
   public ArduinoConfig apply(ArduinoConfig c) {
     super.apply(c);
-
-    if (msg == null) {
+    if (config.connect && config.port != null) {      
       serial = (Serial) startPeer("serial");
-      if (serial == null) {
-        log.error("serial is null");
-      }      
       msg.setSerial(serial);
-      serial.addByteListener(this);
-    } else {
-      // TODO: figure out why this gets called so often.
-      log.info("Init serial we already have a msg class.");
-    }
-
-    if (config.connect && config.port != null) {
+      serial.addByteListener(this);      
       connect(config.port);
     }
 
@@ -2372,13 +2348,8 @@ public ArduinoConfig apply(ArduinoConfig c) {
   public static void main(String[] args) {
     try {
 
-      // Platform.setVirtual(true);
-
       LoggingFactory.init(Level.INFO);
 
-      Runtime runtime = Runtime.getInstance();
-      runtime.saveAllDefaults();
-
       Runtime.start("arduino", "Arduino");
       Runtime.start("webgui", "WebGui");
 
@@ -2387,115 +2358,12 @@ public static void main(String[] args) {
       if (isDone) {
         return;
       }
-      // Platform.setVirtual(true);
-
-      /*
-       * WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
-       * webgui.autoStartBrowser(false); webgui.setPort(8887);
-       * webgui.startService();
-       */
-
-      // Runtime.start("gui", "SwingGui");
-      Serial.listPorts();
-
-      Arduino hub = (Arduino) Runtime.start("controller", "Arduino");
-      Runtime.start("pir", "Pir");
-
-      hub.connect("/dev/ttyACM0");
-
-      // hub.enableAck(false);
-
-      ServoControl sc = (ServoControl) Runtime.start("s1", "Servo");
-      sc.setPin(3);
-      hub.attach(sc);
-      sc = (ServoControl) Runtime.start("s2", "Servo");
-      sc.setPin(9);
-      hub.attach(sc);
-
-      hub.detach();
-
-      // hub.enableAck(true);
-      /*
-       * sc = (ServoControl) Runtime.start("s3", "Servo"); sc.setPin(12);
-       * hub.attach(sc);
-       */
-
-      log.info("here");
-      // hub.connect("COM6"); // uno
-
-      // hub.startTcpServer();
-
-      VirtualArduino vmega = null;
-
-      vmega = (VirtualArduino) Runtime.start("vmega", "VirtualArduino");
-      vmega.connect("COM7");
-      Serial sd = vmega.getSerial();
-      sd.startTcpServer();
-
-      // Runtime.start("webgui", "WebGui");
-
-      Arduino mega = (Arduino) Runtime.start("mega", "Arduino");
-
-      if (mega.isVirtual()) {
-        vmega = mega.getVirtual();
-        vmega.setBoardMega();
-      }
-
-      // mega.getBoardTypes();
-      // mega.setBoardMega();
-      // mega.setBoardUno();
-      mega.connect("COM7");
-
-      /*
-       * Arduino uno = (Arduino) Runtime.start("uno", "Arduino");
-       * uno.connect("COM6");
-       */
-
-      // log.info("port names {}", mega.getPortNames());
-
-      Servo servo = (Servo) Runtime.start("servo", "Servo");
-      // servo.load();
-      log.info("rest is {}", servo.getRest());
-      servo.save();
-      // servo.setPin(8);
-      servo.attach(mega);
-
-      servo.moveTo(90.0);
-
-      /*
-       * servo.moveTo(3); sleep(300); servo.moveTo(130); sleep(300);
-       * servo.moveTo(90); sleep(300);
-       * 
-       * 
-       * // minmax checking
-       * 
-       * servo.invoke("moveTo", 120);
-       */
-
-      /*
-       * mega.attach(servo);
-       * 
-       * servo.moveTo(3);
-       * 
-       * servo.moveTo(30);
-       * 
-       * mega.enablePin("A4");
-       * 
-       * // arduino.setBoardMega();
-       * 
-       * Adafruit16CServoDriver adafruit = (Adafruit16CServoDriver)
-       * Runtime.start("adafruit", "Adafruit16CServoDriver");
-       * adafruit.attach(mega); mega.attach(adafruit);
-       */
-
-      // servo.attach(arduino, 8, 90);
-
-      // Runtime.start("webgui", "WebGui");
-      // Service.sleep(3000);
-
-      // remote.startListening();
 
-      // Runtime.start("webgui", "WebGui");
+//    Platform.setVirtual(true);
+//    Serial sd = vmega.getSerial();
+//    sd.startTcpServer();
+//    Serial.listPorts();
+     
 
     } catch (Exception e) {
       log.error("main threw", e);
diff --git a/src/main/java/org/myrobotlab/service/DiyServo.java b/src/main/java/org/myrobotlab/service/DiyServo.java
index 2e125d8509..ae83c09118 100644
--- a/src/main/java/org/myrobotlab/service/DiyServo.java
+++ b/src/main/java/org/myrobotlab/service/DiyServo.java
@@ -28,7 +28,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
@@ -44,8 +43,6 @@
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.PinArrayControl;
 import org.myrobotlab.service.interfaces.PinListener;
-import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
-import org.myrobotlab.service.interfaces.ServoControl;
 import org.myrobotlab.service.interfaces.ServoEvent;
 import org.slf4j.Logger;
 
@@ -76,7 +73,7 @@
  *         TODO : move is not accurate ( 1° step seem not possible )
  */
 
-public class DiyServo extends AbstractServo<ServoConfig> implements PinListener, ServiceLifeCycleListener {
+public class DiyServo extends AbstractServo<ServoConfig> implements PinListener {
 
   double lastOutput = 0.0;
   /**
@@ -198,16 +195,6 @@ public DiyServo(String n, String id) {
     lastActivityTimeTs = System.currentTimeMillis();
   }
 
-  /*
-   * Update the list of PinArrayControls
-   */
-  @Override
-  public void onRegistered(Registration s) {
-    refreshPinArrayControls();
-    broadcastState();
-
-  }
-
   /**
    * Initiate the PID controller
    */
@@ -224,7 +211,7 @@ void initPid() {
     pid.setSetpoint(pidKey, setPoint);
     pid.startService();
   }
-  
+
   @Override
   public void startService() {
     super.startService();
@@ -232,7 +219,6 @@ public void startService() {
     motorControl = (MotorControl) startPeer("motor");
     initPid();
   }
-  
 
   /**
    * Equivalent to Arduino's Servo.detach() it de-energizes the servo
@@ -694,19 +680,17 @@ public static void main(String[] args) throws InterruptedException {
       // if (done) {
       // return;
       // }
-      
-      WebGui webgui = (WebGui)Runtime.create("webgui", "WebGui");
+
+      WebGui webgui = (WebGui) Runtime.create("webgui", "WebGui");
       webgui.autoStartBrowser(false);
       webgui.startService();
-      
+
       Runtime.start("diy", "DiyServo");
-      
-      
+
       boolean done = true;
       if (done) {
         return;
       }
-      
 
       String port = "COM4";
       Arduino arduino = (Arduino) Runtime.start("arduino", "Arduino");
@@ -788,19 +772,4 @@ protected boolean processMove(Double newPos, boolean blocking, Long timeoutMs) {
     return false;
   }
 
-  @Override
-  public void onCreated(String name) {
-    log.info("created {}", name);
-  }
-
-  @Override
-  public void onStopped(String name) {
-    log.info("stopped {}", name);
-  }
-
-  @Override
-  public void onReleased(String name) {
-    log.info("released {}", name);
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
index 32c04bf29e..e998711e26 100644
--- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
+++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
@@ -2,16 +2,11 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 
-import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.Service;
-import org.myrobotlab.framework.interfaces.MessageListener;
-import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.generics.SlidingWindowList;
 import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
@@ -61,7 +56,7 @@ public class Tuple {
     public Transition transition;
     public StateTransition stateTransition;
   }
-  
+
   public class StateChange {
     /**
      * timestamp
@@ -72,7 +67,7 @@ public class StateChange {
      * current new state
      */
     public String state;
-    
+
     /**
      * event which activated new state
      */
@@ -82,13 +77,12 @@ public class StateChange {
      * source of event
      */
     public String src = getName();
-    
-    
+
     public StateChange(String current, String event) {
       this.state = current;
       this.event = event;
     }
-    
+
     public String toString() {
       return String.format("%s --%s--> %s", last, event, state);
     }
@@ -221,25 +215,25 @@ public String firedEvent(String event) {
   }
 
   /**
-   * gets the current state of this state machine
+   * get the previous state of this state machine
    * 
    * @return
    */
-  public String getCurrent() {
-    if (current != null) {
-      return current.getName();
+  public String getLast() {
+    if (last != null) {
+      return last.getName();
     }
     return null;
   }
 
   /**
-   * get the previous state of this state machine
+   * gets the current state of this state machine
    * 
    * @return
    */
-  public String getLast() {
-    if (last != null) {
-      return last.getName();
+  public String getState() {
+    if (current != null) {
+      return current.getName();
     }
     return null;
   }
@@ -250,7 +244,7 @@ public List<Transition> getTransitions() {
   }
 
   /**
-   * Publishes state change (current, last and event) 
+   * Publishes state change (current, last and event)
    * 
    * @param stateChange
    * @return
@@ -263,7 +257,7 @@ public StateChange publishStateChange(StateChange stateChange) {
   @Override
   public FiniteStateMachineConfig getConfig() {
     super.getConfig();
-    config.current = getCurrent();
+    config.current = getState();
     return config;
   }
 
@@ -361,15 +355,15 @@ public static void main(String[] args) {
 
       // fsm.subscribe("fsm", "publishState");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.setCurrent("neutral");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.fire("ill-event");
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
       fsm.fire("ill-event");
       fsm.fire("ill-event");
@@ -387,7 +381,7 @@ public static void main(String[] args) {
 
       // fsm.removeScheduledEvents();
 
-      log.info("state - {}", fsm.getCurrent());
+      log.info("state - {}", fsm.getState());
 
     } catch (Exception e) {
       log.error("main threw", e);
@@ -419,7 +413,7 @@ public String getPreviousState() {
       return history.get(history.size() - 2).state;
     }
   }
-  
+
   @Override
   public void startService() {
     super.startService();
diff --git a/src/main/java/org/myrobotlab/service/RoboClaw.java b/src/main/java/org/myrobotlab/service/RoboClaw.java
index 83203b99f8..8d22f7ff27 100644
--- a/src/main/java/org/myrobotlab/service/RoboClaw.java
+++ b/src/main/java/org/myrobotlab/service/RoboClaw.java
@@ -16,7 +16,7 @@
 import org.myrobotlab.serial.CRC;
 import org.myrobotlab.service.Pid.PidData;
 import org.myrobotlab.service.abstracts.AbstractMotorController;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.MotorController;
 import org.myrobotlab.service.interfaces.PortConnector;
@@ -55,7 +55,7 @@
  *         this value IS correct
  * 
  */
-public class RoboClaw extends AbstractMotorController<MotorConfig> implements EncoderPublisher, PortConnector, MotorController, SerialDataListener {
+public class RoboClaw extends AbstractMotorController<AbstractMotorControllerConfig> implements EncoderPublisher, PortConnector, MotorController, SerialDataListener {
 
   private static final long serialVersionUID = 1L;
 
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 675123e962..3d3697284a 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -316,12 +316,6 @@ public class Runtime extends Service<RuntimeConfig> implements MessageListener,
 
   protected List<String> configList;
 
-  /***
-   * runtime, security, webgui, perhaps python - we don't want to remove when
-   * releasing config
-   */
-  protected Set<String> startingServices = new HashSet<>();
-
   /**
    * Wraps {@link java.lang.Runtime#availableProcessors()}.
    *
@@ -580,7 +574,7 @@ public final static void createAndStartServices(List<String> services) {
             Logging.logError(e);
           }
         } else {
-          runtime.error(String.format("could not create service %1$s %2$s", name, type));
+          runtime.error(String.format("could not create service %s %s", name, type));
         }
 
       }
@@ -910,9 +904,6 @@ public static Runtime getInstance() {
             // klunky
             Runtime.register(new Registration(runtime));
 
-            // assign, do not apply otherwise there will be
-            // a chicken-egg problem
-            runtime.config = c;
           }
 
           runtime.getRepo().addStatusPublisher(runtime);
@@ -4924,8 +4915,18 @@ static public void releaseConfigPath(String configPath) {
       RuntimeConfig config = CodecUtils.fromYaml(releaseData, RuntimeConfig.class);
       List<String> registry = config.getRegistry();
       Collections.reverse(Arrays.asList(registry));
+      
+      // get starting services if any entered on the command line
+      // -s log Log webgui WebGui ... etc - these will be protected 
+      List<String> startingServices = new ArrayList<>();
+      if (options.services.size() % 2 == 0) {
+        for (int i = 0; i < options.services.size(); i += 2) {
+          startingServices.add(options.services.get(i));
+        }
+      }
+      
       for (String name : registry) {
-        if (Runtime.getInstance().startingServices.contains(name)) {
+        if (startingServices.contains(name)) {
           continue;
         }
         release(name);
diff --git a/src/main/java/org/myrobotlab/service/Servo.java b/src/main/java/org/myrobotlab/service/Servo.java
index 3e0b46ce95..7b59121998 100644
--- a/src/main/java/org/myrobotlab/service/Servo.java
+++ b/src/main/java/org/myrobotlab/service/Servo.java
@@ -61,7 +61,7 @@
  * 
  */
 
-public class Servo extends AbstractServo<ServoConfig> implements ServiceLifeCycleListener {
+public class Servo extends AbstractServo<ServoConfig> {
 
   private static final long serialVersionUID = 1L;
 
@@ -259,95 +259,10 @@ public static void main(String[] args) throws InterruptedException {
         return;
       }
 
-      // runtime.save();
-
-      /*
-       * mega.save(); tilt.save(); pan.save();
-       * 
-       * mega.load(); tilt.load(); pan.load();
-       */
-
-      // TODO - attach before and after connect..
-
-      // mega.setBoardMega();
-
-      // log.info("servo pos {}", tilt.getCurrentInputPos());
-      //
-      // // double pos = 170;
-      // // servo03.setPosition(pos);
-      //
-      // double min = 3;
-      // double max = 170;
-      // double speed = 60; // degree/s
-      //
-      // mega.attach(tilt);
-      // // mega.attach(servo03,3);
-      //
-      // for (int i = 0; i < 100; ++i) {
-      // tilt.moveTo(20.0);
-      // }
-      //
-      // tilt.sweep(min, max, speed);
-
-      /*
-       * Servo servo04 = (Servo) Runtime.start("servo04", "Servo"); Servo
-       * servo05 = (Servo) Runtime.start("servo05", "Servo"); Servo servo06 =
-       * (Servo) Runtime.start("servo06", "Servo"); Servo servo07 = (Servo)
-       * Runtime.start("servo07", "Servo"); Servo servo08 = (Servo)
-       * Runtime.start("servo08", "Servo"); Servo servo09 = (Servo)
-       * Runtime.start("servo09", "Servo"); Servo servo10 = (Servo)
-       * Runtime.start("servo10", "Servo"); Servo servo11 = (Servo)
-       * Runtime.start("servo11", "Servo"); Servo servo12 = (Servo)
-       * Runtime.start("servo12", "Servo");
-       */
-      // Servo servo13 = (Servo) Runtime.start("servo13", "Servo");
-
-      // servo03.attach(mega, 8, 38.0);
-      /*
-       * servo04.attach(mega, 4, 38.0); servo05.attach(mega, 5, 38.0);
-       * servo06.attach(mega, 6, 38.0); servo07.attach(mega, 7, 38.0);
-       * servo08.attach(mega, 8, 38.0); servo09.attach(mega, 9, 38.0);
-       * servo10.attach(mega, 10, 38.0); servo11.attach(mega, 11, 38.0);
-       * servo12.attach(mega, 12, 38.0);
-       */
-
-      // TestCatcher catcher = (TestCatcher)Runtime.start("catcher",
-      // "TestCatcher");
-      // servo03.attach((ServoEventListener)catcher);
-
-      // servo.setPin(12);
-
-      /*
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       * servo.attach(mega, 7, 38.0); servo.attach(mega, 7, 38.0);
-       */
-
-      // servo.sweepDelay = 3;
-      // servo.save();
-      // servo.load();
-      // servo.save();
-      // log.info("sweepDely {}", servo.sweepDelay);
 
     } catch (Exception e) {
       log.error("main threw", e);
     }
   }
 
-  @Override
-  public void onCreated(String name) {
-  }
-
-  @Override
-  public void onStopped(String name) {
-  }
-
-  @Override
-  public void onReleased(String name) {
-  }
-
-
 }
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 5e87cddb05..f50cdc238a 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -34,7 +34,6 @@
 import org.myrobotlab.codec.CodecUtils;
 import org.myrobotlab.framework.MRLListener;
 import org.myrobotlab.framework.Message;
-import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
@@ -62,8 +61,7 @@
  * services are already APIs - perhaps a data API - same as service without the
  * message wrapper
  */
-public class WebGui extends Service<WebGuiConfig>
-    implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
+public class WebGui extends Service<WebGuiConfig> implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
 
   public static class LiveVideoStreamHandler implements Handler {
 
@@ -128,7 +126,7 @@ public Panel(String name, int x, int y, int z) {
    * needed to get the api key to select the appropriate api processor
    * 
    * @param uri
-   *            u
+   *          u
    * @return api key
    * 
    */
@@ -271,9 +269,9 @@ public boolean getAutoStartBrowser() {
    * String broadcast to specific client
    * 
    * @param uuid
-   *             u
+   *          u
    * @param str
-   *             s
+   *          s
    * 
    */
   public void broadcast(String uuid, String str) {
@@ -315,9 +313,7 @@ public Config.Builder getNettosphereConfig() {
         // cert.privateKey()).build();
 
         SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
-        SslContext context = SslContextBuilder
-            .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
-            .sslProvider(SslProvider.JDK)
+        SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK)
             .clientAuth(ClientAuth.NONE).build();
 
         configBuilder.sslContext(context);
@@ -496,8 +492,7 @@ public void handle(AtmosphereResource r) {
         } else if ((bodyData != null) && log.isDebugEnabled()) {
           logData = bodyData;
         }
-        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(),
-            request.getRequestURI(), logData, uuid);
+        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid);
       }
 
       // important persistent connections will have associated routes ...
@@ -575,8 +570,7 @@ public void handle(AtmosphereResource r) {
           }
 
           if (msg.containsHop(getId())) {
-            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender,
-                msg.name, msg.method);
+            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
             return;
           }
 
@@ -920,7 +914,7 @@ public void run() {
    * remotely control UI
    * 
    * @param panel
-   *              - the panel which has been moved or resized
+   *          - the panel which has been moved or resized
    */
   public void savePanel(Panel panel) {
     if (panel.name == null) {
@@ -1107,7 +1101,7 @@ public void releaseService() {
    * Default (false) is to use the CDN
    *
    * @param useLocalResources
-   *                          - true uses local resources fals uses cdn
+   *          - true uses local resources fals uses cdn
    */
   public void useLocalResources(boolean useLocalResources) {
     this.useLocalResources = useLocalResources;
@@ -1183,8 +1177,7 @@ public static void main(String[] args) {
 
     try {
 
-      Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui","intro", "Intro", "python", "Python" });
-      // Runtime.main(new String[] {});
+      Runtime.main(new String[] { "--log-level", "info", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
       // Runtime.main(new String[] { "--install" });
 
       boolean done = true;
@@ -1193,7 +1186,8 @@ public static void main(String[] args) {
       }
 
       // Platform.setVirtual(true);
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
+      // "WebGui",
       // "intro", "Intro", "python", "Python", "-c", "dev" });
       // Runtime.startConfig("dev");
 
@@ -1248,8 +1242,7 @@ public static void main(String[] args) {
       arduino.connect("/dev/ttyACM0");
 
       for (int i = 0; i < 1000; ++i) {
-        webgui.display(
-            "https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
+        webgui.display("https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
       }
 
       // Runtime.setLogLevel("ERROR");
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
index 5f01ebc74f..94380c9711 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractMotorController.java
@@ -8,11 +8,11 @@
 import org.myrobotlab.math.MapperLinear;
 import org.myrobotlab.math.interfaces.Mapper;
 import org.myrobotlab.service.Runtime;
-import org.myrobotlab.service.config.MotorConfig;
+import org.myrobotlab.service.config.AbstractMotorControllerConfig;
 import org.myrobotlab.service.interfaces.MotorControl;
 import org.myrobotlab.service.interfaces.MotorController;
 
-public abstract class AbstractMotorController<C extends MotorConfig> extends Service<C> implements MotorController {
+public abstract class AbstractMotorController<C extends AbstractMotorControllerConfig> extends Service<C> implements MotorController {
 
   /**
    * currently attached motors to this controller
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
index 3378d5e56d..7eee94dc5a 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractServo.java
@@ -6,7 +6,6 @@
 import java.util.Set;
 
 import org.myrobotlab.codec.CodecUtils;
-import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
@@ -217,16 +216,6 @@ public abstract class AbstractServo<C extends ServoConfig> extends Service<C> im
 
   public AbstractServo(String n, String id) {
     super(n, id);
-    // this servo is interested in new services which support either
-    // ServoControllers or EncoderControl interfaces
-    // we subscribe to runtime here for new services
-    subscribeToRuntime("registered");
-    /*
-     * // new feature - // extracting the currentPos from serialized servo
-     * Double lastCurrentPos = null; try { lastCurrentPos = (Double)
-     * loadField("currentPos"); } catch (IOException e) {
-     * log.info("current pos cannot be found in saved file"); }
-     */
     // if no position could be loaded - set to rest
     // we have no "historical" info - assume we are @ rest
     targetPos = rest;
@@ -243,17 +232,6 @@ public AbstractServo(String n, String id) {
     }
   }
 
-  /**
-   * if a new service is added to the system refresh the controllers
-   */
-  @Deprecated /*
-               * lifecycle events not necessary for ui, probably should be
-               * pulled out
-               */
-  public void onStarted(String name) {
-    invoke("refreshControllers");
-  }
-
   /**
    * overloaded routing attach
    */
@@ -456,6 +434,12 @@ public void enable() {
   public void fullSpeed() {
     setSpeed((Double) null);
   }
+  
+  @Override
+  public void setMaxSpeed() {
+    setSpeed((Double) null);
+  }
+
 
   @Override
   public boolean isAutoDisable() {
@@ -697,10 +681,6 @@ public void onEncoderData(EncoderData data) {
     }
   }
 
-  public void onRegistered(Registration s) {
-    refreshControllers();
-  }
-
   /**
    * Servo has the ability to act as an encoder if it is using TimeEncoder.
    * TimeEncoder will use Servo to publish a series of encoder events with
@@ -918,6 +898,11 @@ public void setSpeed(Double degreesPerSecond) {
     // speed = maxSpeed;
     // log.info("Trying to set speed to a value greater than max speed");
     // }
+    
+    if (degreesPerSecond != null && degreesPerSecond < 0) {
+      warn("setting speed to negative value %d ignoring", degreesPerSecond);
+      return;
+    }
 
     speed = degreesPerSecond;
 
@@ -1096,7 +1081,6 @@ public ServoEvent publishServoStopped(String name, Double position) {
   @Override
   public void startService() {
     super.startService();
-    Runtime.getInstance().attachServiceLifeCycleListener(getName());
   }
 
   @Override
diff --git a/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java b/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java
new file mode 100644
index 0000000000..030a1ee4f0
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/config/AbstractMotorControllerConfig.java
@@ -0,0 +1,5 @@
+package org.myrobotlab.service.config;
+
+public class AbstractMotorControllerConfig extends ServiceConfig {
+    // Add your configuration here
+}
diff --git a/src/main/java/org/myrobotlab/service/config/ArduinoConfig.java b/src/main/java/org/myrobotlab/service/config/ArduinoConfig.java
index 1f83bee895..3e4adf8f83 100644
--- a/src/main/java/org/myrobotlab/service/config/ArduinoConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ArduinoConfig.java
@@ -4,7 +4,16 @@
 
 public class ArduinoConfig extends ServiceConfig {
 
+  /**
+   * Port (usb or ip:port) to connect)
+   */
   public String port;
+  
+  /**
+   * If you want the arduino to try to connect
+   * port must not be null.
+   * This is not a status field.
+   */
   public boolean connect;
 
   @Override
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 34c8da7d08..889e91eec9 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -55,7 +55,7 @@ public class InMoov2Config extends ServiceConfig {
    * fire events to the FSM. Checks battery level and sends a heartbeat flash on
    * publishHeartbeat and onHeartbeat at a regular interval
    */
-  public boolean heartbeat = false;
+  public boolean heartbeat = true;
 
   /**
    * flashes the neopixel every time a health check is preformed. green == good
@@ -68,17 +68,17 @@ public class InMoov2Config extends ServiceConfig {
    */
   public long heartbeatInterval = 3000;
 
-  public boolean loadAppsScripts = true;
+  public boolean loadAppsScripts = false;
 
   /**
    * loads all python gesture files in the gesture directory
    */
-  public boolean loadGestures = true;
+  public boolean loadGestures = false;
 
   /**
    * executes all scripts in the init directory on startup
    */
-  public boolean loadInitScripts = true;
+  public boolean loadInitScripts = false;
 
   /**
    * default to null - allow the OS to set it, unless explicilty set
@@ -232,9 +232,17 @@ public Plan getDefault(Plan plan, String name) {
     }
 
     mouthControl.mouth = i01Name + ".mouth";
-
+    
+    UltrasonicSensorConfig ultrasonicLeft = (UltrasonicSensorConfig) plan.get(getPeerName("ultrasonicLeft"));
+    ultrasonicLeft.triggerPin = 64;
+    ultrasonicLeft.echoPin = 63;
+
+    UltrasonicSensorConfig ultrasonicRight = (UltrasonicSensorConfig) plan.get(getPeerName("ultrasonicRight"));
+    ultrasonicRight.triggerPin = 64;
+    ultrasonicRight.echoPin = 63;
+    
+    
     ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot"));
-    chatBot.botDir = "resource/ProgramAB";
 
     chatBot.bots.add("resource/ProgramAB/Alice");
     chatBot.bots.add("resource/ProgramAB/Dr.Who");
@@ -267,8 +275,6 @@ public Plan getDefault(Plan plan, String name) {
       }
     }
 
-    chatBot.currentUserName = "human";
-
     chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
 
     Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3"));
@@ -283,8 +289,7 @@ public Plan getDefault(Plan plan, String name) {
     // setup name references to different services
     MarySpeechConfig mouth = (MarySpeechConfig) plan.get(getPeerName("mouth"));
     mouth.voice = "Mark";
-    mouth.speechRecognizers = new String[] { name + ".ear" };
-
+    
     // == Peer - ear =============================
     // setup name references to different services
     WebkitSpeechRecognitionConfig ear = (WebkitSpeechRecognitionConfig) plan.get(getPeerName("ear"));
@@ -365,16 +370,17 @@ public Plan getDefault(Plan plan, String name) {
     // exists ?
     fsm.current = "boot";
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
-    fsm.transitions.add(new Transition("wake", "idle", "idle"));
-    fsm.transitions.add(new Transition("first_init", "idle", "idle"));
+    // fsm.transitions.add(new Transition("wake", "idle", "idle")); wake, setup, nor sleep should be affected by idle
+    fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
     fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("random", "idle", "idle"));
     fsm.transitions.add(new Transition("idle", "sleep", "sleep"));
     fsm.transitions.add(new Transition("sleep", "wake", "wake"));
     fsm.transitions.add(new Transition("sleep", "power_down", "power_down"));
     fsm.transitions.add(new Transition("idle", "power_down", "power_down"));
-    fsm.transitions.add(new Transition("wake", "first_init", "first_init"));
-    fsm.transitions.add(new Transition("idle", "first_init", "first_init"));
+    fsm.transitions.add(new Transition("wake", "setup", "setup"));
+    fsm.transitions.add(new Transition("wake", "idle", "idle"));
+    fsm.transitions.add(new Transition("idle", "setup", "setup"));
     // power_down to shutdown
     // fsm.transitions.add(new Transition("systemCheck", "systemCheckFinished",
     // "awake"));
@@ -520,7 +526,11 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishPlayAudioFile", getPeerName("audioPlayer")));
     listeners.add(new Listener("publishPlayAnimation", getPeerName("neoPixel")));
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
-    listeners.add(new Listener("publishProcessMessage", getPeerName("py4j"), "onPythonMessage"));
+    // listeners.add(new Listener("publishProcessMessage",
+    // getPeerName("python"), "onPythonMessage"));
+    listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
+    
+    listeners.add(new Listener("publishPython", "python"));
 
     // InMoov2 --to--> InMoov2
     listeners.add(new Listener("publishMoveHead", getPeerName("head"), "onMove"));
@@ -533,6 +543,8 @@ public Plan getDefault(Plan plan, String name) {
     // service --to--> InMoov2
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
+    
+    htmlFilter.listeners.add(new Listener("publishText", name));
 
     OakDConfig oakd = (OakDConfig) plan.get(getPeerName("oakd"));
     oakd.listeners.add(new Listener("publishClassification", name));
@@ -544,7 +556,11 @@ public Plan getDefault(Plan plan, String name) {
     // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
 
     // Needs upcoming pr
-    // fsm.listeners.add(new Listener("publishStateChange", name));
+    fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
+    
+    // peer --to--> peer
+    mouth.listeners.add(new Listener("publishStartSpeaking", getPeerName("ear")));
+    mouth.listeners.add(new Listener("publishEndSpeaking", getPeerName("ear")));
 
     return plan;
   }
diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
index ce9ae14033..0cd4dbc839 100644
--- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
@@ -10,11 +10,6 @@ public class ProgramABConfig extends ServiceConfig {
   @Deprecated /* unused text filters */
   public String[] textFilters;
   
-  /**
-   * a directory ProgramAB will scan for new bots
-   */
-  public String botDir;
-
   /**
    * explicit bot directories
    */
diff --git a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
index 42db50689b..44dc5a0c5b 100644
--- a/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/SabertoothConfig.java
@@ -2,7 +2,7 @@
 
 import org.myrobotlab.framework.Plan;
 
-public class SabertoothConfig extends MotorConfig {
+public class SabertoothConfig extends AbstractMotorControllerConfig {
 
   public String port;
   public boolean connect = false;
diff --git a/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java b/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java
index f48ec59e49..2c23767e33 100644
--- a/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/UltrasonicSensorConfig.java
@@ -10,11 +10,13 @@ public class UltrasonicSensorConfig extends ServiceConfig {
   /**
    * pulse pin
    */
+  @Deprecated /* Pins need to be Strings eg "D64" */
   public Integer triggerPin;
   
   /**
    * listening pin
    */
+  @Deprecated /* Pins need to be Strings eg "D63" */
   public Integer echoPin;
   
   /**
diff --git a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
index 1e140ce167..379154cd6e 100644
--- a/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/ServoControl.java
@@ -404,7 +404,12 @@ public interface ServoControl extends AbsolutePositionControl, EncoderListener,
   /**
    * disable speed control and move the servos at full speed.
    */
-  @Deprecated /* implement setSpeed(null) */
+  @Deprecated
   void fullSpeed();
-
+  
+  /**
+   * 
+   */
+  void setMaxSpeed();
+  
 }
diff --git a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
index 1b8f134beb..73d284cdb6 100644
--- a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
@@ -16,7 +16,7 @@ public OpenCVMeta() {
 
     addDescription("OpenCV (computer vision) service wrapping many of the functions and filters of OpenCV");
     addCategory("video", "vision", "sensors");
-    String javaCvVersion = "1.5.7";
+    String javaCvVersion = "1.5.8";
     // addDependency("org.bytedeco", "javacv", javaCvVersion);
     addDependency("org.bytedeco", "javacv-platform", javaCvVersion);
     addDependency("org.bytedeco", "javacpp", javaCvVersion);
diff --git a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
index 5d15601b58..fb83ed0777 100644
--- a/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
+++ b/src/test/java/org/myrobotlab/config/ConfigUtilsTest.java
@@ -6,6 +6,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.myrobotlab.framework.StartYml;
+import org.myrobotlab.io.FileIO;
 import org.myrobotlab.service.Runtime;
 
 public class ConfigUtilsTest {
@@ -13,6 +14,8 @@ public class ConfigUtilsTest {
   @Before
   public void beforeTest() {
     Runtime.releaseAll(true, true);
+    // remove config
+    FileIO.rm("data/config/default");
   }
 
   @Test
diff --git a/src/test/java/org/myrobotlab/framework/BlockingTest.java b/src/test/java/org/myrobotlab/framework/BlockingTest.java
index b4ce03e8da..3b9788645b 100644
--- a/src/test/java/org/myrobotlab/framework/BlockingTest.java
+++ b/src/test/java/org/myrobotlab/framework/BlockingTest.java
@@ -28,13 +28,13 @@ public void blockingTest() throws Exception {
 
     Message msg = Message.createMessage("thower07", "catcher07", "onInt", 3);
     Integer ret = (Integer)thower07.sendBlocking(msg, null);
-    assertEquals(simpleName, 3, (int)ret);
+    assertEquals(3, (int)ret);
 
     long startTime = System.currentTimeMillis();
     msg = Message.createMessage("thower07", "catcher07", "waitForThis", new Object[] {7, 1000});
     ret = (Integer)thower07.sendBlocking(msg, null);
     assertTrue("1s process", System.currentTimeMillis() - startTime > 500);
-    assertEquals(simpleName, 7, (int)ret);
+    assertEquals(7, (int)ret);
 
     Runtime.release("catcher07");
     Runtime.release("thower07");
diff --git a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
index f2715dc90d..d0e2c57735 100644
--- a/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
+++ b/src/test/java/org/myrobotlab/framework/repo/RepoTest.java
@@ -28,6 +28,10 @@ public static void lastCleanup() {
     repo.clear();
     installed = false;
   }
+  
+  public String getName() {
+    return "RepoTest";
+  }
 
   @Override
   public void broadcastStatus(Status status) {
diff --git a/src/test/java/org/myrobotlab/service/ArduinoTest.java b/src/test/java/org/myrobotlab/service/ArduinoTest.java
index 7a6422d29d..00cee5e0a7 100644
--- a/src/test/java/org/myrobotlab/service/ArduinoTest.java
+++ b/src/test/java/org/myrobotlab/service/ArduinoTest.java
@@ -62,7 +62,6 @@ private void assertVirtualPinValue(VirtualArduino virtual, int address, int valu
     }
   }
 
-  @Override
   public String getName() {
     return "arduinoTest";
   }
diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index 7c8add5923..f089f2e453 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -7,31 +7,24 @@
 import java.util.Map;
 
 import org.junit.Before;
-import org.myrobotlab.framework.Service;
+import org.junit.Test;
 import org.myrobotlab.service.Random.RandomMessage;
+import org.myrobotlab.test.AbstractTest;
 
-public class RandomTest extends AbstractServiceTest {
+public class RandomTest extends AbstractTest {
 
-  @Override /*
-             * FIXME - this assumes a single service is in the test - which
-             * rarely happens - seems not useful and silly
-             */
-  public Service createService() throws Exception {
-    return (Service) Runtime.start("randomTest", "Random");
-  }
-  
   @Before /* before each test */
   public void setUp() throws IOException {
     // remove all services - also resets config name to DEFAULT effectively
     Runtime.releaseAll(true, true);
-      // clean our config directory
+    // clean our config directory
     // Runtime.removeConfig("RandomTest");
     // set our config
     Runtime.setConfig("RandomTest");
+    Runtime.start("randomTest", "Random");
   }
-  
 
-  @Override
+  @Test
   public void testService() throws Exception {
     Clock clock = (Clock) Runtime.start("clock", "Clock");
     Random random = (Random) Runtime.start("randomTest", "Random");
@@ -46,62 +39,63 @@ public void testService() throws Exception {
     sleep(1000);
 
     assertTrue("should have method", random.getKeySet().contains("clock.setInterval"));
-    
+
     assertTrue(String.format("random method 1 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 1 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
-    
+    assertTrue(String.format("random method 1 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
+
     random.remove("clock.setInterval");
-    
+
     assertTrue("should not have method", !random.getKeySet().contains("clock.setInterval"));
 
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
     random.addRandom(0, 200, "clock", "startClock");
-    
+
     sleep(500);
     assertTrue("clock should be started 1", clock.isClockRunning());
-    
+
     // disable all of a services random events
     random.disable("clock.startClock");
-    clock.stopClock();
     sleep(250);
+    clock.stopClock();
     assertTrue("clock should not be started 1", !clock.isClockRunning());
-    
+
     // enable all of a service's random events
     random.enable("clock.startClock");
     sleep(250);
     assertTrue("clock should be started 2", clock.isClockRunning());
-    
+
     // disable one method - leave other enabled
     random.disable("clock.startClock");
     clock.stopClock();
-    clock.setInterval(9999);
     sleep(200);
+    clock.setInterval(9999);
     assertTrue("clock should not be started 3", !clock.isClockRunning());
     assertTrue(String.format("random method 2 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 2 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 2 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
 
     // disable all
     random.disable();
     sleep(200);
     clock.setInterval(9999);
-    assertTrue("clock should not be started 4", !clock.isClockRunning());   
-    assertEquals(9999, (long)clock.getInterval());
+    assertTrue("clock should not be started 4", !clock.isClockRunning());
+    assertEquals(9999, (long) clock.getInterval());
 
-    // re-enable all that were previously enabled but not explicitly disabled ones
+    // re-enable all that were previously enabled but not explicitly disabled
+    // ones
     random.enable();
     sleep(1000);
     assertTrue("clock should not be started 5", !clock.isClockRunning());
     assertTrue(String.format("random method 3 should be %d => 5000 values", clock.getInterval()), 5000 <= clock.getInterval());
-    assertTrue(String.format("random method 3 should be %d <= 10000 values",clock.getInterval()) , clock.getInterval() <= 10000);
+    assertTrue(String.format("random method 3 should be %d <= 10000 values", clock.getInterval()), clock.getInterval() <= 10000);
 
     clock.stopClock();
     random.purge();
-        
+
     Map<String, RandomMessage> events = random.getRandomEvents();
     assertTrue(events.size() == 0);
-    
+
     random.addRandom("named task", 200, 500, "clock", "setInterval", 100, 1000, 10);
-    
+
     clock.releaseService();
     random.releaseService();
 
diff --git a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
index 07e1775110..4bdb93fe2c 100644
--- a/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
+++ b/src/test/java/org/myrobotlab/service/RuntimeProcessTest.java
@@ -21,12 +21,15 @@ public class RuntimeProcessTest extends AbstractTest {
 
   @Before
   public void setUp() {
-    // LoggingFactory.init("WARN");
   }
 
   public boolean contains(ByteArrayOutputStream out, String str) {
     return new String(out.toByteArray()).contains(str);
   }
+  
+  public String getName() {
+    return "RuntimeProcessTest";
+  }
 
   @Test
   public void cliTest() throws Exception {
diff --git a/src/test/java/org/myrobotlab/service/ServoTest.java b/src/test/java/org/myrobotlab/service/ServoTest.java
index 8d96052e2e..819c3718d6 100644
--- a/src/test/java/org/myrobotlab/service/ServoTest.java
+++ b/src/test/java/org/myrobotlab/service/ServoTest.java
@@ -73,7 +73,7 @@ public void autoDisableAfterAttach() {
   @Test
   public void disabledMove() throws Exception {
     // take off speed control
-    servo.fullSpeed();
+    servo.setMaxSpeed();
     servo.moveTo(0.0);
     servo.setInverted(false);
     Service.sleep(1000);
diff --git a/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java b/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
index ba1d028096..110ff1ac4a 100755
--- a/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
+++ b/src/test/java/org/myrobotlab/service/VirtualArduinoTest.java
@@ -30,6 +30,10 @@ public Service createService() {
     VirtualArduino service = (VirtualArduino) Runtime.start("virtualArduino", "VirtualArduino");
     return service;
   }
+  
+  public String getName() {
+    return "VirtualArduinoTest";
+  }
 
   @Override
   public void testService() throws Exception {
diff --git a/src/test/java/org/myrobotlab/service/WebGuiTest.java b/src/test/java/org/myrobotlab/service/WebGuiTest.java
index 643485fc0c..02918dedba 100644
--- a/src/test/java/org/myrobotlab/service/WebGuiTest.java
+++ b/src/test/java/org/myrobotlab/service/WebGuiTest.java
@@ -5,7 +5,6 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
 import java.util.List;
 
 import org.junit.Before;
@@ -22,8 +21,8 @@
 public class WebGuiTest extends AbstractTest {
 
   public final static Logger log = LoggerFactory.getLogger(WebGui.class);
-  
-  // FIXME - DO A WEBSOCKET TEST 
+
+  // FIXME - DO A WEBSOCKET TEST
 
   @Before
   public void setUp() {
@@ -31,11 +30,11 @@ public void setUp() {
     webgui2.autoStartBrowser(false);
     webgui2.setPort(8889);
     webgui2.startService();
-    
-    Runtime.start("servoApiTest","Servo");
+
+    Runtime.start("servoApiTest", "Servo");
     Runtime.start("pythonApiTest", "Python");
     // need to wait for the OS to open the port
-    Service.sleep(3);
+    Service.sleep(200);
   }
 
   @Test
@@ -46,7 +45,7 @@ public void getTest() {
     String ret = new String(bytes);
     assertTrue(ret.contains("days"));
   }
-  
+
   @Test
   public void getTestWithParameter() throws UnsupportedEncodingException {
 
@@ -56,13 +55,12 @@ public void getTestWithParameter() throws UnsupportedEncodingException {
     assertTrue(ret.contains("true"));
   }
 
-
-// FIXME - ADD WHEN POST API IS WORKY
-// FIXME object non primitive (no string) post
+  // FIXME - ADD WHEN POST API IS WORKY
+  // FIXME object non primitive (no string) post
 
   @Test
   public void postTest() {
-    
+
     // 1st post - simple input - simple return
     String postBody = "[\"runtime\"]";
     byte[] bytes = Http.post("http://localhost:8889/api/service/runtime/getFullName", postBody);
@@ -70,7 +68,7 @@ public void postTest() {
     assertNotNull(bytes);
     String ret = new String(bytes);
     assertTrue(ret.contains("@"));
-    
+
     // second post - simple input - complex return
     postBody = "[\"runtime\"]";
     bytes = Http.post("http://localhost:8889/api/service/runtime/getService", postBody);
@@ -78,29 +76,31 @@ public void postTest() {
     assertNotNull(bytes);
     ret = new String(bytes);
     assertTrue(ret.contains("@"));
-    
-    
+
     // second post - simple input (including array of strings) - complex return
-    // FIXME uncomment when ready - callbacks are not possible through the rest api
-    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.toString exceeded
-    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for proxyName@remoteId.getFullName exceeded
-//    postBody = "[\"remoteId\", \"proxyName\", \"py:myService\",[\"org.myrobotlab.framework.interfaces.ServiceInterface\"]]";
-//    bytes = Http.post("http://localhost:8889/api/service/runtime/register", postBody);
-//    sleep(200);
-//    assertNotNull(bytes);
-//    ret = new String(bytes);
-//    assertTrue(ret.contains("remoteId"));
-    
-    
-    
+    // FIXME uncomment when ready - callbacks are not possible through the rest
+    // api
+    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for
+    // proxyName@remoteId.toString exceeded
+    // org.myrobotlab.framework.TimeoutException: timeout of 3000 for
+    // proxyName@remoteId.getFullName exceeded
+    // postBody = "[\"remoteId\", \"proxyName\",
+    // \"py:myService\",[\"org.myrobotlab.framework.interfaces.ServiceInterface\"]]";
+    // bytes = Http.post("http://localhost:8889/api/service/runtime/register",
+    // postBody);
+    // sleep(200);
+    // assertNotNull(bytes);
+    // ret = new String(bytes);
+    // assertTrue(ret.contains("remoteId"));
+
     // post non primitive non string object
     MRLListener listener = new MRLListener("getRegistry", "runtime@webguittest", "onRegistry");
-    postBody = "[" + CodecUtils.toJson(listener) + "]";    
+    postBody = "[" + CodecUtils.toJson(listener) + "]";
     // postBody = "[\"runtime\"]";
     bytes = Http.post("http://localhost:8889/api/service/runtime/addListener", postBody);
     sleep(200);
     assertNotNull(bytes);
-    
+
     Runtime runtime = Runtime.getInstance();
     boolean found = false;
     List<MRLListener> check = runtime.getNotifyList("getRegistry");
@@ -108,9 +108,9 @@ public void postTest() {
       if (check.get(i).equals(listener)) {
         found = true;
       }
-    }    
+    }
     assertTrue("listener not found !", found);
-    
+
   }
 
   @Test
@@ -138,7 +138,7 @@ public void servoApiTest() {
 
   @Test
   public void urlEncodingTest() {
-    //exec("print \"hello\"")
+    // exec("print \"hello\"")
     byte[] bytes = Http.get("http://localhost:8889/api/service/pythonApiTest/exec/%22print+%5C%22hello%5C%22%22");
     String ret = new String(bytes);
     assertEquals("true", ret);
@@ -147,16 +147,19 @@ public void urlEncodingTest() {
   @Test
   public void sendBlockingTest() throws InterruptedException, TimeoutException {
     String retVal = "retVal";
-    // Put directly in blocking list because sendBlocking() won't use it for local services
+    // Put directly in blocking list because sendBlocking() won't use it for
+    // local
+    // services
     Runtime.getInstance().getInbox().blockingList.put("runtime.onBlocking", new Object[1]);
     Object[] blockingListRet = Runtime.getInstance().getInbox().blockingList.get("runtime.onBlocking");
 
     // Delay in a new thread so we can get our wait() call in first
     new Thread(() -> {
       try {
-        Thread.sleep(50);
-      } catch (InterruptedException ignored) {}
-      Http.post("http://localhost:8889/api/service/runtime/onBlocking", "[\""+retVal+"\"]");
+        Thread.sleep(100);
+      } catch (InterruptedException ignored) {
+      }
+      Http.post("http://localhost:8889/api/service/runtime/onBlocking", "[\"" + retVal + "\"]");
     }).start();
 
     long timeout = 1000;
@@ -170,6 +173,5 @@ public void sendBlockingTest() throws InterruptedException, TimeoutException {
 
     assertEquals(retVal, blockingListRet[0]);
   }
-  
-  
+
 }
diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index d782797c9b..7279a8fe98 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -14,82 +14,61 @@
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Rule;
-import org.junit.rules.TestName;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
 import org.myrobotlab.codec.CodecUtils;
-import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.interfaces.Attachable;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.Runtime;
 import org.myrobotlab.service.config.RuntimeConfig;
 import org.slf4j.Logger;
 
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-
 public class AbstractTest {
 
-  /** cached network test value for tests */
-  static Boolean hasInternet = null;
+  /**
+   * cached network test value for tests
+   */
+  protected static Boolean hasInternet = null;
 
+  /**
+   * Install dependencies once per process, same process
+   * will not check.  A new process will use the libraries/serviceData.json
+   * to determine if deps are satisfied
+   */
   protected static boolean installed = false;
 
-  public final static Logger log = LoggerFactory.getLogger(AbstractTest.class);
-
-  static private boolean logWarnTestHeader = false;
+  protected final static Logger log = LoggerFactory.getLogger(AbstractTest.class);
 
-  private static boolean releaseRemainingThreads = false;
+  protected static transient Set<Thread> threadSetStart = null;
 
-  protected transient Queue<Object> queue = new LinkedBlockingQueue<>();
-
-  static transient Set<Thread> threadSetStart = null;
-
-  protected Set<Attachable> attached = new HashSet<>();
-
-  @Rule
-  public final TestName testName = new TestName();
-
-  static public String simpleName;
-
-  private static boolean lineFeedFooter = true;
-  
   @Rule
   public TestWatcher watchman = new TestWatcher() {
-      @Override
-      protected void starting(Description description) {
-          System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
-      }
+    @Override
+    protected void starting(Description description) {
+      System.out.println("Starting: " + description.getClassName() + "." + description.getMethodName());
+    }
 
-      @Override
-      protected void succeeded(Description description) {
-         // System.out.println("Succeeded: " + description.getMethodName());
-      }
+    @Override
+    protected void succeeded(Description description) {
+      // System.out.println("Succeeded: " + description.getMethodName());
+    }
 
-      @Override
-      protected void failed(Throwable e, Description description) {
-          System.out.println("Failed: " + description.getMethodName());
-      }
+    @Override
+    protected void failed(Throwable e, Description description) {
+      System.out.println("Failed: " + description.getMethodName());
+    }
 
-      @Override
-      protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
-          System.out.println("Skipped: " + description.getMethodName());
-      }
+    @Override
+    protected void skipped(org.junit.AssumptionViolatedException e, Description description) {
+      System.out.println("Skipped: " + description.getMethodName());
+    }
 
-      @Override
-      protected void finished(Description description) {
-          System.out.println("Finished: " + description.getMethodName());
-      }
+    @Override
+    protected void finished(Description description) {
+      System.out.println("Finished: " + description.getMethodName());
+    }
   };
 
-  public String getSimpleName() {
-    return simpleName;
-  }
-
-  public String getName() {
-    return testName.getMethodName();
-  }
-
   static public boolean hasInternet() {
     if (hasInternet == null) {
       hasInternet = Runtime.hasInternet();
@@ -120,23 +99,23 @@ public static void main(String[] args) {
 
   @BeforeClass
   public static void setUpAbstractTest() throws Exception {
-    
+
     // setup runtime resource = src/main/resources/resource
     File runtimeYml = new File("data/config/default/runtime.yml");
-//    if (!runtimeYml.exists()) {
-      runtimeYml.getParentFile().mkdirs();
-      RuntimeConfig rc = new RuntimeConfig();
-      rc.resource = "src/main/resources/resource";
-      String yml = CodecUtils.toYaml(rc);
-      
-      FileOutputStream fos = null;
-      fos = new FileOutputStream(runtimeYml);
-      fos.write(yml.getBytes());
-      fos.close();
-      
-//    }
-
-      Runtime.getInstance().setVirtual(true);
+    // if (!runtimeYml.exists()) {
+    runtimeYml.getParentFile().mkdirs();
+    RuntimeConfig rc = new RuntimeConfig();
+    rc.resource = "src/main/resources/resource";
+    String yml = CodecUtils.toYaml(rc);
+
+    FileOutputStream fos = null;
+    fos = new FileOutputStream(runtimeYml);
+    fos.write(yml.getBytes());
+    fos.close();
+
+    // }
+
+    Runtime.getInstance().setVirtual(true);
 
     String junitLogLevel = System.getProperty("junit.logLevel");
     if (junitLogLevel != null) {
@@ -171,16 +150,7 @@ public static void sleep(long sleepTimeMs) {
   @AfterClass
   public static void tearDownAbstractTest() throws Exception {
     log.info("tearDownAbstractTest");
-
     releaseServices();
-
-    if (logWarnTestHeader) {
-      log.warn("=========== finished test {} ===========", simpleName);
-    }
-
-    if (lineFeedFooter) {
-      System.out.println();
-    }
   }
 
   static protected void installAll() {
@@ -197,8 +167,7 @@ static protected void installAll() {
    */
   public static void releaseServices() {
 
-    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(),
-        Arrays.toString(Runtime.getServiceNames()));
+    log.info("end of test - id {} remaining services {}", Runtime.getInstance().getId(), Arrays.toString(Runtime.getServiceNames()));
 
     // release all including runtime - be careful of default runtime.yml
     Runtime.releaseAll(true, true);
@@ -212,19 +181,8 @@ public static void releaseServices() {
     Set<Thread> threadSetEnd = Thread.getAllStackTraces().keySet();
     Set<String> threadsRemaining = new TreeSet<>();
     for (Thread thread : threadSetEnd) {
-      if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName())
-          && !"runtime".equals(thread.getName())) {
-        if (releaseRemainingThreads) {
-          log.warn("interrupting thread {}", thread.getName());
-          thread.interrupt();
-          /*
-           * if (useDeprecatedThreadStop) { thread.stop(); }
-           */
-        } else {
-          // log.warn("thread {} marked as straggler - should be killed",
-          // thread.getName());
+      if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName()) && !"runtime".equals(thread.getName())) {
           threadsRemaining.add(thread.getName());
-        }
       }
     }
     if (threadsRemaining.size() > 0) {
@@ -236,13 +194,6 @@ public static void releaseServices() {
     // Arrays.toString(Runtime.getServiceNames()));
   }
 
-  public AbstractTest() {
-    simpleName = this.getClass().getSimpleName();
-    if (logWarnTestHeader) {
-      log.info("=========== starting test {} ===========", this.getClass().getSimpleName());
-    }
-  }
-
   public void setVirtual() {
     Runtime.getInstance().setVirtual(true);
   }

From c6c2f143afaee6e3281cd6b024f5d82358cec3a2 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:06:23 -0800
Subject: [PATCH 067/131] re-adding deprecated ProgramAB.botDir

---
 .../java/org/myrobotlab/service/config/ProgramABConfig.java    | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
index 0cd4dbc839..95d5f4a0e3 100644
--- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
@@ -10,6 +10,9 @@ public class ProgramABConfig extends ServiceConfig {
   @Deprecated /* unused text filters */
   public String[] textFilters;
   
+  @Deprecated /* unnecessary and unwanted - specify bots directly */
+  public String botDir;
+  
   /**
    * explicit bot directories
    */

From 4a6957433e123b8b15590fa4dfb8115ad0de382b Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:25:06 -0800
Subject: [PATCH 068/131] npe fix for runtime.apply(c)

---
 src/main/java/org/myrobotlab/service/Runtime.java | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 3d3697284a..9ae1c8a17b 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -911,14 +911,10 @@ public static Runtime getInstance() {
           // extract resources "if a jar"
           FileIO.extractResources();
           runtime.startInteractiveMode();
-
-          if (Runtime.options.install != null) {
-            // minimal processed runtime - return it
-            return runtime;
+          if (c != null) {
+            runtime.apply(c);
           }
 
-          runtime.apply(c);
-
           if (options.services != null) {
             log.info("command line override for services created");
             createAndStartServices(options.services);

From 6e02e9fd40dbbe44037562b9cb89e54c3b93a7e6 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:25:48 -0800
Subject: [PATCH 069/131] npe fix for runtime.apply(c)

---
 src/main/java/org/myrobotlab/service/Runtime.java | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 3d3697284a..9ae1c8a17b 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -911,14 +911,10 @@ public static Runtime getInstance() {
           // extract resources "if a jar"
           FileIO.extractResources();
           runtime.startInteractiveMode();
-
-          if (Runtime.options.install != null) {
-            // minimal processed runtime - return it
-            return runtime;
+          if (c != null) {
+            runtime.apply(c);
           }
 
-          runtime.apply(c);
-
           if (options.services != null) {
             log.info("command line override for services created");
             createAndStartServices(options.services);

From 76c8b00bf0fbae71589778d8b07afe019c5be2d2 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:44:51 -0800
Subject: [PATCH 070/131] updated javacpp to 1.5.8 and updated template

---
 pom.xml                                       | 25 ++-----------------
 .../resource/framework/pom.xml.template       | 23 +++++++++--------
 2 files changed, 14 insertions(+), 34 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9b89a805fc..fb8bc15b6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -484,10 +484,6 @@
     <!-- Duplicate entry for org.myrobotlab.audio-voice-effects-1.0 skipping -->
     <!-- IndianTts end -->
 
-    <!-- IntegratedMovement begin -->
-    <!-- Duplicate entry for fr.inmoov-inmoov2-null skipping -->
-    <!-- IntegratedMovement end -->
-
     <!-- JFugue begin -->
     <dependency>
       <groupId>jfugue</groupId>
@@ -1079,10 +1075,10 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>javacv-platform</artifactId>
-      <version>1.5.7</version>
+      <version>1.5.8</version>
       <scope>provided</scope>
     </dependency>
-    <!-- Duplicate entry for org.bytedeco-javacpp-1.5.7 skipping -->
+    <!-- Duplicate entry for org.bytedeco-javacpp-1.5.8 skipping -->
     <!-- Duplicate entry for com.github.sarxos-webcam-capture-driver-v4l4j-0.3.13-SNAPSHOT skipping -->
     <!-- Duplicate entry for org.apache.commons-commons-lang3-3.3.2 skipping -->
     <dependency>
@@ -1557,15 +1553,6 @@
     </dependency>
     <!-- Sphinx end -->
 
-    <!-- Tensorflow begin -->
-    <dependency>
-      <groupId>org.tensorflow</groupId>
-      <artifactId>tensorflow</artifactId>
-      <version>1.8.0</version>
-      <scope>provided</scope>
-    </dependency>
-    <!-- Tensorflow end -->
-
     <!-- TesseractOcr begin -->
     <dependency>
       <groupId>org.bytedeco</groupId>
@@ -1690,10 +1677,6 @@
     </dependency>
     <!-- WebGui end -->
 
-    <!-- WebSocketConnector begin -->
-    <!-- Duplicate entry for javax.websocket-javax.websocket-api-1.1 skipping -->
-    <!-- WebSocketConnector end -->
-
     <!-- Webcam begin -->
     <dependency>
       <groupId>com.github.sarxos</groupId>
@@ -1751,10 +1734,6 @@
     </dependency>
     <!-- WolframAlpha end -->
 
-    <!-- WorkE begin -->
-    <!-- skipping org.myrobotlab worke org.myrobotlab-worke-null null version/latest -->
-    <!-- WorkE end -->
-
     <!-- Xmpp begin -->
     <dependency>
       <groupId>org.igniterealtime.smack</groupId>
diff --git a/src/main/resources/resource/framework/pom.xml.template b/src/main/resources/resource/framework/pom.xml.template
index ca584c5ba0..29e5f21eb9 100644
--- a/src/main/resources/resource/framework/pom.xml.template
+++ b/src/main/resources/resource/framework/pom.xml.template
@@ -83,7 +83,7 @@
       <!-- force overriding property at command line, use ${maven.build.timestamp}-->
       <timestamp>${maven.build.timestamp}</timestamp>
       <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format>      
-      <version>${project.version}</version>   
+      <version>${version}</version>   
       <GitBranch>${git.branch}</GitBranch>
       <username>${NODE_NAME}</username>      
       <platform>${NODE_LABELS}</platform>      
@@ -193,7 +193,7 @@
                         </goals>
                         <configuration>
                             <finalName>myrobotlab</finalName>
-                            <!-- finalName>myrobotlab-${git.branch}-${project.version}</finalName -->
+                            <!-- finalName>myrobotlab-${git.branch}-${version}</finalName -->
                             <shadedArtifactAttached>true</shadedArtifactAttached>
                             <shadedClassifierName>myrobotlab-full</shadedClassifierName>
                             <minimizeJar>false</minimizeJar>
@@ -207,10 +207,10 @@
                                         implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                         <manifestEntries>
                                           <Main-Class>org.myrobotlab.service.Runtime</Main-Class>
-                                          <Major-Version>${project.version}</Major-Version>
-                                          <Implementation-Version>${project.version}</Implementation-Version>                               
+                                          <Major-Version>${version}</Major-Version>
+                                          <Implementation-Version>${version}</Implementation-Version>                               
               
-                        <Build-Version>${project.version}</Build-Version>
+                        <Build-Version>${version}</Build-Version>
                         <Build-Time>${maven.build.timestamp}</Build-Time>
                         <Build-Host>${agent.name}</Build-Host>
                         <Build-User>${user.name}</Build-User>
@@ -331,20 +331,21 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <groupId>org.apache.maven.plugins</groupId>
         <!-- do not upgrade this version jacoco will break -->
-        <version>2.18</version>
+        <version>3.2.2</version>
         <configuration>
           <!-- critical for jacoco to have original argLine prefixed here-->
-          <argLine>${argLine} -Djava.library.path=libraries/native -Djna.library.path=libraries/native</argLine>
+          <argLine>${argLine} -Djava.library.path=libraries/native
+            -Djna.library.path=libraries/native</argLine>
           <includes>
             <include>**/*Test.java</include>
           </includes>
           <excludes>
             <exclude>**/integration/*</exclude>
             <!-- unfortunately not testing OpenCV is required for ci to work -->
-            <exclude>**/OpenCV*</exclude>
           </excludes>
-             <systemPropertyVariables>
-              </systemPropertyVariables>
+          <!-- required so surefire doesnt die talking to the agent over stdin -->
+          <forkNode
+            implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory" />
         </configuration>
         <executions>
           <execution>
@@ -425,7 +426,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-report-plugin</artifactId>
-        <version>2.18</version>
+        <version>3.2.2</version>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>

From 5caf15d9ae8c44e5bba62f2d14b4da84bce87449 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 07:51:10 -0800
Subject: [PATCH 071/131] synching javacpp 1.5.8 updating template

---
 pom.xml                                       | 25 ++-----------------
 .../resource/framework/pom.xml.template       | 23 +++++++++--------
 2 files changed, 14 insertions(+), 34 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9b89a805fc..fb8bc15b6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -484,10 +484,6 @@
     <!-- Duplicate entry for org.myrobotlab.audio-voice-effects-1.0 skipping -->
     <!-- IndianTts end -->
 
-    <!-- IntegratedMovement begin -->
-    <!-- Duplicate entry for fr.inmoov-inmoov2-null skipping -->
-    <!-- IntegratedMovement end -->
-
     <!-- JFugue begin -->
     <dependency>
       <groupId>jfugue</groupId>
@@ -1079,10 +1075,10 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>javacv-platform</artifactId>
-      <version>1.5.7</version>
+      <version>1.5.8</version>
       <scope>provided</scope>
     </dependency>
-    <!-- Duplicate entry for org.bytedeco-javacpp-1.5.7 skipping -->
+    <!-- Duplicate entry for org.bytedeco-javacpp-1.5.8 skipping -->
     <!-- Duplicate entry for com.github.sarxos-webcam-capture-driver-v4l4j-0.3.13-SNAPSHOT skipping -->
     <!-- Duplicate entry for org.apache.commons-commons-lang3-3.3.2 skipping -->
     <dependency>
@@ -1557,15 +1553,6 @@
     </dependency>
     <!-- Sphinx end -->
 
-    <!-- Tensorflow begin -->
-    <dependency>
-      <groupId>org.tensorflow</groupId>
-      <artifactId>tensorflow</artifactId>
-      <version>1.8.0</version>
-      <scope>provided</scope>
-    </dependency>
-    <!-- Tensorflow end -->
-
     <!-- TesseractOcr begin -->
     <dependency>
       <groupId>org.bytedeco</groupId>
@@ -1690,10 +1677,6 @@
     </dependency>
     <!-- WebGui end -->
 
-    <!-- WebSocketConnector begin -->
-    <!-- Duplicate entry for javax.websocket-javax.websocket-api-1.1 skipping -->
-    <!-- WebSocketConnector end -->
-
     <!-- Webcam begin -->
     <dependency>
       <groupId>com.github.sarxos</groupId>
@@ -1751,10 +1734,6 @@
     </dependency>
     <!-- WolframAlpha end -->
 
-    <!-- WorkE begin -->
-    <!-- skipping org.myrobotlab worke org.myrobotlab-worke-null null version/latest -->
-    <!-- WorkE end -->
-
     <!-- Xmpp begin -->
     <dependency>
       <groupId>org.igniterealtime.smack</groupId>
diff --git a/src/main/resources/resource/framework/pom.xml.template b/src/main/resources/resource/framework/pom.xml.template
index ca584c5ba0..29e5f21eb9 100644
--- a/src/main/resources/resource/framework/pom.xml.template
+++ b/src/main/resources/resource/framework/pom.xml.template
@@ -83,7 +83,7 @@
       <!-- force overriding property at command line, use ${maven.build.timestamp}-->
       <timestamp>${maven.build.timestamp}</timestamp>
       <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format>      
-      <version>${project.version}</version>   
+      <version>${version}</version>   
       <GitBranch>${git.branch}</GitBranch>
       <username>${NODE_NAME}</username>      
       <platform>${NODE_LABELS}</platform>      
@@ -193,7 +193,7 @@
                         </goals>
                         <configuration>
                             <finalName>myrobotlab</finalName>
-                            <!-- finalName>myrobotlab-${git.branch}-${project.version}</finalName -->
+                            <!-- finalName>myrobotlab-${git.branch}-${version}</finalName -->
                             <shadedArtifactAttached>true</shadedArtifactAttached>
                             <shadedClassifierName>myrobotlab-full</shadedClassifierName>
                             <minimizeJar>false</minimizeJar>
@@ -207,10 +207,10 @@
                                         implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                         <manifestEntries>
                                           <Main-Class>org.myrobotlab.service.Runtime</Main-Class>
-                                          <Major-Version>${project.version}</Major-Version>
-                                          <Implementation-Version>${project.version}</Implementation-Version>                               
+                                          <Major-Version>${version}</Major-Version>
+                                          <Implementation-Version>${version}</Implementation-Version>                               
               
-                        <Build-Version>${project.version}</Build-Version>
+                        <Build-Version>${version}</Build-Version>
                         <Build-Time>${maven.build.timestamp}</Build-Time>
                         <Build-Host>${agent.name}</Build-Host>
                         <Build-User>${user.name}</Build-User>
@@ -331,20 +331,21 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <groupId>org.apache.maven.plugins</groupId>
         <!-- do not upgrade this version jacoco will break -->
-        <version>2.18</version>
+        <version>3.2.2</version>
         <configuration>
           <!-- critical for jacoco to have original argLine prefixed here-->
-          <argLine>${argLine} -Djava.library.path=libraries/native -Djna.library.path=libraries/native</argLine>
+          <argLine>${argLine} -Djava.library.path=libraries/native
+            -Djna.library.path=libraries/native</argLine>
           <includes>
             <include>**/*Test.java</include>
           </includes>
           <excludes>
             <exclude>**/integration/*</exclude>
             <!-- unfortunately not testing OpenCV is required for ci to work -->
-            <exclude>**/OpenCV*</exclude>
           </excludes>
-             <systemPropertyVariables>
-              </systemPropertyVariables>
+          <!-- required so surefire doesnt die talking to the agent over stdin -->
+          <forkNode
+            implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory" />
         </configuration>
         <executions>
           <execution>
@@ -425,7 +426,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-report-plugin</artifactId>
-        <version>2.18</version>
+        <version>3.2.2</version>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>

From 46f2f274c4913779b7c85c44dcb7a3a6b572056d Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 08:07:09 -0800
Subject: [PATCH 072/131] syching javacpp 1.5.8 tesseract and deeplearning4j

---
 pom.xml                                                     | 6 +++---
 .../org/myrobotlab/service/meta/Deeplearning4jMeta.java     | 2 +-
 .../java/org/myrobotlab/service/meta/TesseractOcrMeta.java  | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index fb8bc15b6e..77ddf5a516 100644
--- a/pom.xml
+++ b/pom.xml
@@ -215,7 +215,7 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>javacpp</artifactId>
-      <version>1.5.7</version>
+      <version>1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -1557,13 +1557,13 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>tesseract</artifactId>
-      <version>5.0.1-1.5.7</version>
+      <version>5.2.0-1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>tesseract-platform</artifactId>
-      <version>5.0.1-1.5.7</version>
+      <version>5.2.0-1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
diff --git a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
index 56fe4f8ad5..23fc4aeff8 100644
--- a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
@@ -23,7 +23,7 @@ public Deeplearning4jMeta() {
     addCategory("ai");
 
     // Force javacpp 1.5.3 to resolve conflict between dl4j and javacv
-    addDependency("org.bytedeco", "javacpp", "1.5.7");
+    addDependency("org.bytedeco", "javacpp", "1.5.8");
 
     // REMOVED FOR COLLISION
     // addDependency("org.bytedeco", "openblas", "0.3.17-" + "1.5.6");
diff --git a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
index 5099e60d79..8c8d9a2554 100644
--- a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
@@ -14,7 +14,7 @@ public class TesseractOcrMeta extends MetaData {
    */
   public TesseractOcrMeta() {
 
-    String tesseractVersion = "5.0.1-1.5.7";
+    String tesseractVersion = "5.2.0-1.5.8";
     addDescription("Optical character recognition - the ability to read");
     addCategory("ai", "vision");
     addDependency("org.bytedeco", "tesseract", tesseractVersion);

From 1851976911325868cc418fd1a8cf2f35eee17e11 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 08:09:43 -0800
Subject: [PATCH 073/131] synching javacpp 1.5.8 with deeplearning4j and
 tesseract

---
 pom.xml                                                     | 6 +++---
 .../org/myrobotlab/service/meta/Deeplearning4jMeta.java     | 2 +-
 .../java/org/myrobotlab/service/meta/TesseractOcrMeta.java  | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index fb8bc15b6e..77ddf5a516 100644
--- a/pom.xml
+++ b/pom.xml
@@ -215,7 +215,7 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>javacpp</artifactId>
-      <version>1.5.7</version>
+      <version>1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
@@ -1557,13 +1557,13 @@
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>tesseract</artifactId>
-      <version>5.0.1-1.5.7</version>
+      <version>5.2.0-1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.bytedeco</groupId>
       <artifactId>tesseract-platform</artifactId>
-      <version>5.0.1-1.5.7</version>
+      <version>5.2.0-1.5.8</version>
       <scope>provided</scope>
     </dependency>
     <dependency>
diff --git a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
index 56fe4f8ad5..23fc4aeff8 100644
--- a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
@@ -23,7 +23,7 @@ public Deeplearning4jMeta() {
     addCategory("ai");
 
     // Force javacpp 1.5.3 to resolve conflict between dl4j and javacv
-    addDependency("org.bytedeco", "javacpp", "1.5.7");
+    addDependency("org.bytedeco", "javacpp", "1.5.8");
 
     // REMOVED FOR COLLISION
     // addDependency("org.bytedeco", "openblas", "0.3.17-" + "1.5.6");
diff --git a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
index 5099e60d79..8c8d9a2554 100644
--- a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
@@ -14,7 +14,7 @@ public class TesseractOcrMeta extends MetaData {
    */
   public TesseractOcrMeta() {
 
-    String tesseractVersion = "5.0.1-1.5.7";
+    String tesseractVersion = "5.2.0-1.5.8";
     addDescription("Optical character recognition - the ability to read");
     addCategory("ai", "vision");
     addDependency("org.bytedeco", "tesseract", tesseractVersion);

From d7fad5a5a8ce192e92709fded2f623ca0d17cd23 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 08:17:23 -0800
Subject: [PATCH 074/131] changed imports on tesseract

---
 src/main/java/org/myrobotlab/service/TesseractOcr.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/TesseractOcr.java b/src/main/java/org/myrobotlab/service/TesseractOcr.java
index b444ceadec..180801d75c 100644
--- a/src/main/java/org/myrobotlab/service/TesseractOcr.java
+++ b/src/main/java/org/myrobotlab/service/TesseractOcr.java
@@ -1,7 +1,7 @@
 package org.myrobotlab.service;
 
-import static org.bytedeco.leptonica.global.lept.pixDestroy;
-import static org.bytedeco.leptonica.global.lept.pixRead;
+import static org.bytedeco.leptonica.global.leptonica.pixDestroy;
+import static org.bytedeco.leptonica.global.leptonica.pixRead;
 
 import java.awt.image.BufferedImage;
 import java.io.File;

From 2e6c89c31ca97090df9b082e448d309985db0c36 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 08:17:59 -0800
Subject: [PATCH 075/131] changed imports on tesseract

---
 src/main/java/org/myrobotlab/service/TesseractOcr.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/TesseractOcr.java b/src/main/java/org/myrobotlab/service/TesseractOcr.java
index b444ceadec..180801d75c 100644
--- a/src/main/java/org/myrobotlab/service/TesseractOcr.java
+++ b/src/main/java/org/myrobotlab/service/TesseractOcr.java
@@ -1,7 +1,7 @@
 package org.myrobotlab.service;
 
-import static org.bytedeco.leptonica.global.lept.pixDestroy;
-import static org.bytedeco.leptonica.global.lept.pixRead;
+import static org.bytedeco.leptonica.global.leptonica.pixDestroy;
+import static org.bytedeco.leptonica.global.leptonica.pixRead;
 
 import java.awt.image.BufferedImage;
 import java.io.File;

From 72903143d98dff1013ffdb455e7e62f879bf02e6 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 08:55:52 -0800
Subject: [PATCH 076/131] fixed test - was counting threads after new runtime
 started

---
 src/test/java/org/myrobotlab/test/AbstractTest.java | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java
index 7279a8fe98..69e0b611a0 100644
--- a/src/test/java/org/myrobotlab/test/AbstractTest.java
+++ b/src/test/java/org/myrobotlab/test/AbstractTest.java
@@ -173,8 +173,6 @@ public static void releaseServices() {
     Runtime.releaseAll(true, true);
     // wait for draining threads
     sleep(100);
-    // resets runtime with fresh new instance
-    Runtime.getInstance();
 
     // check threads - kill stragglers
     // Set<Thread> stragglers = new HashSet<Thread>();
@@ -189,9 +187,9 @@ public static void releaseServices() {
       log.warn("{} straggling threads remain [{}]", threadsRemaining.size(), String.join(",", threadsRemaining));
     }
 
-    // log.warn("end of test - id {} remaining services after release {}",
-    // Platform.getLocalInstance().getId(),
-    // Arrays.toString(Runtime.getServiceNames()));
+    // resets runtime with fresh new instance
+    Runtime.getInstance();
+
   }
 
   public void setVirtual() {

From 9e1710ad742f0363c235d136b5c1dbf1bf198950 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 09:47:15 -0800
Subject: [PATCH 077/131] more unit test cleanup

---
 .../org/myrobotlab/service/AudioFile.java     |  8 ++++
 .../java/org/myrobotlab/service/NeoPixel.java |  1 +
 .../service/ServiceInterfaceTest.java         | 37 ++++++++++---------
 3 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java
index 3997f6960e..dbf2131bcf 100644
--- a/src/main/java/org/myrobotlab/service/AudioFile.java
+++ b/src/main/java/org/myrobotlab/service/AudioFile.java
@@ -384,6 +384,14 @@ public List<File> getFiles(String subDir, boolean recurse) {
     return new ArrayList<File>();
   }
 
+  @Override
+  public void releaseService() {
+    super.releaseService();
+    for (AudioProcessor processor: processors.values()) {
+      processor.stopPlaying();
+    }
+  }
+  
   public AudioData repeat(String filename) {
     return repeat(filename, -1);
   }
diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java
index 21a05cbdc5..a5a22c4782 100644
--- a/src/main/java/org/myrobotlab/service/NeoPixel.java
+++ b/src/main/java/org/myrobotlab/service/NeoPixel.java
@@ -810,6 +810,7 @@ public void playIronman() {
   public void releaseService() {
     super.releaseService();
     clear();
+    worker.stop();
   }
 
   @Override
diff --git a/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java b/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java
index 806985daa6..6f5c117b03 100644
--- a/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java
+++ b/src/test/java/org/myrobotlab/service/ServiceInterfaceTest.java
@@ -56,7 +56,7 @@ private boolean serviceHasWebPage(String service) {
 
   private boolean serviceInterfaceTest(String service) throws IOException {
     // see if we can start/stop and release the service.
-    
+
     // set a configuration path
     Runtime.setConfig("serviceInterfaceTest");
 
@@ -65,9 +65,9 @@ private boolean serviceInterfaceTest(String service) throws IOException {
       log.warn("Runtime Create returned a null service for {}", service);
       return false;
     }
-    System.out.println("Service Test:" + service);
+    System.out.println("ServiceInterface Test:" + service);
 
-    if (service.equals("As5048AEncoder")){
+    if (service.equals("As5048AEncoder")) {
       log.info("here");
     }
 
@@ -85,7 +85,7 @@ private boolean serviceInterfaceTest(String service) throws IOException {
     foo.startService();
     foo.save();
     // foo.load(); SHOULD NOT BE USED !
-    // foo.apply(); <-  THIS SHOULD BE IMPLEMENTED
+    // foo.apply(); <- THIS SHOULD BE IMPLEMENTED
     foo.stopService();
 
     foo.releaseService();
@@ -103,9 +103,9 @@ public final void testAllServices() throws ClassNotFoundException, IOException {
     ArrayList<String> servicesNotInServiceDataJson = new ArrayList<String>();
 
     HashSet<String> blacklist = new HashSet<String>();
-    blacklist.add("OpenNi");    
-    blacklist.add("As5048AEncoder");    
-    blacklist.add("IntegratedMovement");    
+    blacklist.add("OpenNi");
+    blacklist.add("As5048AEncoder");
+    blacklist.add("IntegratedMovement");
     blacklist.add("VirtualDevice");
     blacklist.add("Joystick");
     blacklist.add("GoogleAssistant");
@@ -145,8 +145,9 @@ public final void testAllServices() throws ClassNotFoundException, IOException {
     // FIXME - must have different thread (prefix script) which runs a timer -
     // script REQUIRED to complete in 4 minutes ... or BOOM it fails
 
-    // sts.clear();
-    // sts.add(sd.getServiceType("org.myrobotlab.service.InMoov"));
+    // USEFUL FOR DEBUGGING SINGLE SERVICE
+//    sts.clear();
+//    sts.add(ServiceData.getMetaData("org.myrobotlab.service.NeoPixel"));
 
     for (MetaData serviceType : sts) {
       // test single service
@@ -164,7 +165,7 @@ public final void testAllServices() throws ClassNotFoundException, IOException {
         continue;
       }
       // log.info("Testing Service: {}", service);
-      
+
       System.out.println("testing " + service);
 
       MetaData st = ServiceData.getMetaData("org.myrobotlab.service." + service);
@@ -222,14 +223,14 @@ public final void testAllServices() throws ClassNotFoundException, IOException {
 
     }
 
-    log.info("----------------------------------------------");
-    log.info("Service Report");
-    log.info("Number of Services:           {}", numServices);
-    log.info("Number of Startable Services: {}", numStartable);
-    log.info("Number of Services Pages      {}", numServicePages);
-    log.info("Number of Scripts:            {}", numScripts);
-    log.info("Number of Scripts Worky:      {}", numScriptsWorky);
-    log.info("----------------------------------------------");
+    System.out.println("----------------------------------------------");
+    System.out.println("Service Report");
+    System.out.println(String.format("Number of Services:           %d", numServices));
+    System.out.println(String.format("Number of Startable Services: %d", numStartable));
+    System.out.println(String.format("Number of Services Pages      %d", numServicePages));
+    System.out.println(String.format("Number of Scripts:            %d", numScripts));
+    System.out.println(String.format("Number of Scripts Worky:      %d", numScriptsWorky));
+    System.out.println("----------------------------------------------");
 
     for (String s : servicesThatDontStartProperly) {
       log.warn("FAILED ON START:" + s);

From 1fa4e6231ac999b8cb12ab34ccb1a6a499e5d1ba Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 11:21:39 -0800
Subject: [PATCH 078/131] requested fixes

---
 src/main/java/org/myrobotlab/config/ConfigUtils.java       | 2 +-
 src/main/java/org/myrobotlab/framework/Service.java        | 2 +-
 src/main/java/org/myrobotlab/service/Runtime.java          | 7 +++++++
 src/main/java/org/myrobotlab/service/Serial.java           | 4 ++--
 src/main/java/org/myrobotlab/service/VirtualArduino.java   | 1 -
 .../java/org/myrobotlab/service/config/InMoov2Config.java  | 7 ++++---
 src/test/java/org/myrobotlab/service/VertxTest.java        | 2 +-
 7 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 69f850e602..7b793452ac 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -97,7 +97,7 @@ public static StartYml loadStartYml() {
       try {
         FileIO.toFile("start.yml", defaultStartFile);
       } catch (IOException e) {
-        log.error("could not save start.yml");
+        log.error("could not save start.yml", e);
       }
     } else {
       // load start.yml
diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 3f7acbf755..682acebbc5 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -510,7 +510,7 @@ public String getResourcePath(String additionalPath) {
    */
 
   static public String getResourceRoot() {
-    return ConfigUtils.getResourceRoot();//Runtime.getInstance().getConfig().resource;
+    return ConfigUtils.getResourceRoot();
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 9ae1c8a17b..9b76d3bf65 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -1549,6 +1549,11 @@ static public void install(String serviceType, Boolean blocking) {
       if (blocking == null) {
         blocking = false;
       }
+      
+      if (installerThread != null) {
+        log.error("another request to install dependencies, 1st request has not completed");
+        return;
+      }
 
       installerThread = new Thread() {
         @Override
@@ -1571,6 +1576,8 @@ public void run() {
       } else {
         installerThread.start();
       }
+      
+      installerThread = null;
     }
   }
 
diff --git a/src/main/java/org/myrobotlab/service/Serial.java b/src/main/java/org/myrobotlab/service/Serial.java
index c6fccb9d84..a263429144 100644
--- a/src/main/java/org/myrobotlab/service/Serial.java
+++ b/src/main/java/org/myrobotlab/service/Serial.java
@@ -1120,8 +1120,8 @@ public void stopRecording() {
   }
 
   @Override
-  public void stopService() {
-    super.stopService();
+  public void releaseService() {
+    super.releaseService();
     disconnect();
     stopRecording();
   }
diff --git a/src/main/java/org/myrobotlab/service/VirtualArduino.java b/src/main/java/org/myrobotlab/service/VirtualArduino.java
index f1879aa5b6..54671dc8e2 100644
--- a/src/main/java/org/myrobotlab/service/VirtualArduino.java
+++ b/src/main/java/org/myrobotlab/service/VirtualArduino.java
@@ -260,7 +260,6 @@ public void releaseService() {
     }
     // sleep(300);
     disconnect();
-    super.releaseService();
   }
 
   public Serial getSerial() {
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 889e91eec9..1e884c1e4a 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -188,6 +188,7 @@ public Plan getDefault(Plan plan, String name) {
     addDefaultPeerConfig(plan, name, "openWeatherMap", "OpenWeatherMap", false);
     addDefaultPeerConfig(plan, name, "pid", "Pid", false);
     addDefaultPeerConfig(plan, name, "pir", "Pir", false);
+    addDefaultGlobalConfig(plan, "python", "python", "Python");
     addDefaultPeerConfig(plan, name, "py4j", "Py4j", false);
     addDefaultPeerConfig(plan, name, "random", "Random", false);
     addDefaultPeerConfig(plan, name, "right", "Arduino", false);
@@ -370,7 +371,7 @@ public Plan getDefault(Plan plan, String name) {
     // exists ?
     fsm.current = "boot";
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
-    // fsm.transitions.add(new Transition("wake", "idle", "idle")); wake, setup, nor sleep should be affected by idle
+    // setup, nor sleep should be affected by idle
     fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
     fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("random", "idle", "idle"));
@@ -528,9 +529,9 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
     // listeners.add(new Listener("publishProcessMessage",
     // getPeerName("python"), "onPythonMessage"));
-    listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
+    listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
     
-    listeners.add(new Listener("publishPython", "python"));
+    listeners.add(new Listener("publishPython", getPeerName("python")));
 
     // InMoov2 --to--> InMoov2
     listeners.add(new Listener("publishMoveHead", getPeerName("head"), "onMove"));
diff --git a/src/test/java/org/myrobotlab/service/VertxTest.java b/src/test/java/org/myrobotlab/service/VertxTest.java
index eba95ee828..f0ac18e022 100644
--- a/src/test/java/org/myrobotlab/service/VertxTest.java
+++ b/src/test/java/org/myrobotlab/service/VertxTest.java
@@ -58,7 +58,7 @@ public void getTest() {
       assertNotNull(bytes);
       String ret = new String(bytes);
       assertTrue(ret.contains("days"));
-      System.out.println(String.format("%d", i));
+      log.info(String.format("%d", i));
     }
   }
 

From 2dbde9107082ee9a6a1eda8000113293bdff75da Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 17 Feb 2024 11:25:04 -0800
Subject: [PATCH 079/131] requested fixes

---
 src/main/java/org/myrobotlab/config/ConfigUtils.java       | 2 +-
 src/main/java/org/myrobotlab/framework/Service.java        | 2 +-
 src/main/java/org/myrobotlab/service/Runtime.java          | 7 +++++++
 src/main/java/org/myrobotlab/service/Serial.java           | 4 ++--
 src/main/java/org/myrobotlab/service/VirtualArduino.java   | 1 -
 .../java/org/myrobotlab/service/config/InMoov2Config.java  | 7 ++++---
 src/test/java/org/myrobotlab/service/VertxTest.java        | 2 +-
 7 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 69f850e602..7b793452ac 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -97,7 +97,7 @@ public static StartYml loadStartYml() {
       try {
         FileIO.toFile("start.yml", defaultStartFile);
       } catch (IOException e) {
-        log.error("could not save start.yml");
+        log.error("could not save start.yml", e);
       }
     } else {
       // load start.yml
diff --git a/src/main/java/org/myrobotlab/framework/Service.java b/src/main/java/org/myrobotlab/framework/Service.java
index 3f7acbf755..682acebbc5 100644
--- a/src/main/java/org/myrobotlab/framework/Service.java
+++ b/src/main/java/org/myrobotlab/framework/Service.java
@@ -510,7 +510,7 @@ public String getResourcePath(String additionalPath) {
    */
 
   static public String getResourceRoot() {
-    return ConfigUtils.getResourceRoot();//Runtime.getInstance().getConfig().resource;
+    return ConfigUtils.getResourceRoot();
   }
 
   /**
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 9ae1c8a17b..9b76d3bf65 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -1549,6 +1549,11 @@ static public void install(String serviceType, Boolean blocking) {
       if (blocking == null) {
         blocking = false;
       }
+      
+      if (installerThread != null) {
+        log.error("another request to install dependencies, 1st request has not completed");
+        return;
+      }
 
       installerThread = new Thread() {
         @Override
@@ -1571,6 +1576,8 @@ public void run() {
       } else {
         installerThread.start();
       }
+      
+      installerThread = null;
     }
   }
 
diff --git a/src/main/java/org/myrobotlab/service/Serial.java b/src/main/java/org/myrobotlab/service/Serial.java
index c6fccb9d84..a263429144 100644
--- a/src/main/java/org/myrobotlab/service/Serial.java
+++ b/src/main/java/org/myrobotlab/service/Serial.java
@@ -1120,8 +1120,8 @@ public void stopRecording() {
   }
 
   @Override
-  public void stopService() {
-    super.stopService();
+  public void releaseService() {
+    super.releaseService();
     disconnect();
     stopRecording();
   }
diff --git a/src/main/java/org/myrobotlab/service/VirtualArduino.java b/src/main/java/org/myrobotlab/service/VirtualArduino.java
index f1879aa5b6..54671dc8e2 100644
--- a/src/main/java/org/myrobotlab/service/VirtualArduino.java
+++ b/src/main/java/org/myrobotlab/service/VirtualArduino.java
@@ -260,7 +260,6 @@ public void releaseService() {
     }
     // sleep(300);
     disconnect();
-    super.releaseService();
   }
 
   public Serial getSerial() {
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 889e91eec9..1e884c1e4a 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -188,6 +188,7 @@ public Plan getDefault(Plan plan, String name) {
     addDefaultPeerConfig(plan, name, "openWeatherMap", "OpenWeatherMap", false);
     addDefaultPeerConfig(plan, name, "pid", "Pid", false);
     addDefaultPeerConfig(plan, name, "pir", "Pir", false);
+    addDefaultGlobalConfig(plan, "python", "python", "Python");
     addDefaultPeerConfig(plan, name, "py4j", "Py4j", false);
     addDefaultPeerConfig(plan, name, "random", "Random", false);
     addDefaultPeerConfig(plan, name, "right", "Arduino", false);
@@ -370,7 +371,7 @@ public Plan getDefault(Plan plan, String name) {
     // exists ?
     fsm.current = "boot";
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
-    // fsm.transitions.add(new Transition("wake", "idle", "idle")); wake, setup, nor sleep should be affected by idle
+    // setup, nor sleep should be affected by idle
     fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
     fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("random", "idle", "idle"));
@@ -528,9 +529,9 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
     // listeners.add(new Listener("publishProcessMessage",
     // getPeerName("python"), "onPythonMessage"));
-    listeners.add(new Listener("publishProcessMessage", "python", "onPythonMessage"));
+    listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
     
-    listeners.add(new Listener("publishPython", "python"));
+    listeners.add(new Listener("publishPython", getPeerName("python")));
 
     // InMoov2 --to--> InMoov2
     listeners.add(new Listener("publishMoveHead", getPeerName("head"), "onMove"));
diff --git a/src/test/java/org/myrobotlab/service/VertxTest.java b/src/test/java/org/myrobotlab/service/VertxTest.java
index eba95ee828..f0ac18e022 100644
--- a/src/test/java/org/myrobotlab/service/VertxTest.java
+++ b/src/test/java/org/myrobotlab/service/VertxTest.java
@@ -58,7 +58,7 @@ public void getTest() {
       assertNotNull(bytes);
       String ret = new String(bytes);
       assertTrue(ret.contains("days"));
-      System.out.println(String.format("%d", i));
+      log.info(String.format("%d", i));
     }
   }
 

From a032e8e3758fe4384d05504242c95c46e5b6a84e Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 18 Feb 2024 12:18:33 -0800
Subject: [PATCH 080/131] channels for discordbot and programab

---
 .../org/myrobotlab/service/DiscordBot.java    | 22 ++++++++++++++++--
 .../org/myrobotlab/service/ProgramAB.java     | 10 ++++++++
 .../service/config/ProgramABConfig.java       |  8 +++++++
 .../service/interfaces/UtteranceListener.java | 23 +++++++++++++++++++
 .../interfaces/UtterancePublisher.java        | 19 ++++++++++++++-
 5 files changed, 79 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/DiscordBot.java b/src/main/java/org/myrobotlab/service/DiscordBot.java
index f3aa3560cf..3a52145523 100644
--- a/src/main/java/org/myrobotlab/service/DiscordBot.java
+++ b/src/main/java/org/myrobotlab/service/DiscordBot.java
@@ -1,7 +1,6 @@
 package org.myrobotlab.service;
 
 import java.util.List;
-import java.util.Set;
 
 import org.myrobotlab.discord.MrlDiscordBotListener;
 import org.myrobotlab.framework.Service;
@@ -9,7 +8,6 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.service.config.DiscordBotConfig;
-import org.myrobotlab.service.config.ServiceConfig;
 import org.myrobotlab.service.data.ImageData;
 import org.myrobotlab.service.data.Utterance;
 import org.myrobotlab.service.interfaces.ImageListener;
@@ -86,6 +84,10 @@ public void attach(Attachable attachable) {
       attachUtteranceListener(attachable.getName());
     }
 
+    if (attachable instanceof UtterancePublisher) {
+      attachUtterancePublisher(attachable.getName());
+    }
+
     if (attachable instanceof ImagePublisher) {
       attachImagePublisher(attachable.getName());
     }
@@ -95,6 +97,22 @@ public void attach(Attachable attachable) {
     }
   }
 
+  @Override
+  public void detach(Attachable attachable) {
+    if (attachable instanceof UtteranceListener) {
+      detachUtteranceListener(attachable.getName());
+    }
+
+    if (attachable instanceof UtterancePublisher) {
+      detachUtterancePublisher(attachable.getName());
+    }
+
+    if (attachable instanceof ImagePublisher) {
+      detachImagePublisher(attachable.getName());
+    }
+
+  }
+
   @Override
   public DiscordBotConfig getConfig() {
     super.getConfig();
diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java
index 63dc903174..6b0aad8f00 100644
--- a/src/main/java/org/myrobotlab/service/ProgramAB.java
+++ b/src/main/java/org/myrobotlab/service/ProgramAB.java
@@ -1349,6 +1349,16 @@ public void onUtterance(Utterance utterance) throws Exception {
         if (!config.sleep) {
           shouldIRespond = true;
         }
+        
+        if (config.channels != null && config.channels.size() > 0) {
+          // assume false
+          shouldIRespond = false;
+          for (String channelName : config.channels) {
+            if (channelName.equals(utterance.channelName)) {
+              shouldIRespond = true;
+            }
+          }
+        }
       }
     }
 
diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
index 0cd4dbc839..168e5550fd 100644
--- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
@@ -35,6 +35,14 @@ public class ProgramABConfig extends ServiceConfig {
    */
   public boolean sleep = false;
   
+  
+  /**
+   * Specific list of channels ProgramAB will respond to, if not defined, then
+   * ProgramAB will respond to all channels
+   */
+  public List<String> channels = new ArrayList<>();
+  
+  
   /**
    * topic to start with, if null then topic will be loaded from predicates of 
    * a new session if available, this means a config/{username}.predicates.txt 
diff --git a/src/main/java/org/myrobotlab/service/interfaces/UtteranceListener.java b/src/main/java/org/myrobotlab/service/interfaces/UtteranceListener.java
index 9cbee1a146..041c8928f1 100755
--- a/src/main/java/org/myrobotlab/service/interfaces/UtteranceListener.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/UtteranceListener.java
@@ -11,5 +11,28 @@ public interface UtteranceListener {
   public String getName();
 
   public void onUtterance(Utterance utterance) throws Exception;
+  
+  
+  default public void attachUtterancePublisher(UtterancePublisher publisher) {
+    attachUtterancePublisher(publisher.getName());
+  }
+
+  // Default way to attach an image listener so implementing classes need
+  // not worry about these details.
+  default public void attachUtterancePublisher(String name) {
+    send(name, "attachUtteranceListener", getName());
+  }
+
+  default public void detachUtterancePublisher(UtterancePublisher publisher) {
+    detachUtterancePublisher(publisher.getName());
+  }
+
+  // Default way to attach an image listener so implementing classes need
+  // not worry about these details.
+  default public void detachUtterancePublisher(String name) {
+    send(name, "detachUtteranceListener", getName());
+  }
+
+  public void send(String name, String method, Object... data);
 
 }
diff --git a/src/main/java/org/myrobotlab/service/interfaces/UtterancePublisher.java b/src/main/java/org/myrobotlab/service/interfaces/UtterancePublisher.java
index ec467c6c61..330144fe54 100755
--- a/src/main/java/org/myrobotlab/service/interfaces/UtterancePublisher.java
+++ b/src/main/java/org/myrobotlab/service/interfaces/UtterancePublisher.java
@@ -1,5 +1,6 @@
 package org.myrobotlab.service.interfaces;
 
+import org.myrobotlab.framework.interfaces.NameProvider;
 import org.myrobotlab.service.data.Utterance;
 
 /**
@@ -9,7 +10,7 @@
  *
  * 
  */
-public interface UtterancePublisher {
+public interface UtterancePublisher extends NameProvider {
 
   // These are all the methods that the utterance publisher should produce.
   public static String[] publishMethods = new String[] { "publishUtterance" };
@@ -25,8 +26,24 @@ default public void attachUtteranceListener(String name) {
       addListener(publishMethod, name);
     }
   }
+  
+  default public void detachUtteranceListener(UtteranceListener display) {
+    detachUtteranceListener(display.getName());
+  }
+
+  // Default way to attach an image listener so implementing classes need
+  // not worry about these details.
+  default public void detachUtteranceListener(String name) {
+    for (String publishMethod : UtterancePublisher.publishMethods) {
+      removeListener(publishMethod, name);
+    }
+  }
+
 
   // Add the addListener method to the interface all services implement this.
   public void addListener(String topicMethod, String callbackName);
+  
+  public void removeListener(String topicMethod, String callbackName);
+
 
 }

From cbd84fa819334ebb964ab16fc4138b7d78d02fdb Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 18 Feb 2024 12:19:49 -0800
Subject: [PATCH 081/131] onerror added and log.publishErrors

---
 .../java/org/myrobotlab/service/InMoov2.java  | 147 +++++++-----------
 src/main/java/org/myrobotlab/service/Log.java |  10 ++
 .../java/org/myrobotlab/service/OakD.java     |   4 +-
 .../service/config/InMoov2Config.java         |   6 +-
 4 files changed, 75 insertions(+), 92 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 8aac57a03e..b83c909e47 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -24,7 +24,6 @@
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
-import org.myrobotlab.framework.StaticType;
 import org.myrobotlab.framework.Status;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.io.FileIO;
@@ -38,8 +37,6 @@
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
-import org.myrobotlab.service.config.OpenCVConfig;
-import org.myrobotlab.service.config.SpeechSynthesisConfig;
 import org.myrobotlab.service.data.JoystickData;
 import org.myrobotlab.service.data.Locale;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -56,8 +53,7 @@
 import org.slf4j.Logger;
 
 public class InMoov2 extends Service<InMoov2Config>
-    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider,
-    IKJointAngleListener {
+    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
 
   public class Heart implements Runnable {
     private final ReentrantLock lock = new ReentrantLock();
@@ -130,7 +126,7 @@ public Heartbeat(InMoov2 inmoov) {
    * This method will load a python file into the python interpreter.
    * 
    * @param file
-   *             file to load
+   *          file to load
    * @return success/failure
    */
   @Deprecated /* use execScript - this doesn't handle resources correctly */
@@ -279,8 +275,7 @@ public static void main(String[] args) {
 
   public InMoov2(String n, String id) {
     super(n, id);
-    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
-        "pt-PT", "tr-TR");
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -294,8 +289,7 @@ public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
 
-      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT",
-          "fi-FI", "pt-PT", "tr-TR");
+      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
 
       if (c.locale != null) {
         setLocale(c.locale);
@@ -660,7 +654,7 @@ public boolean exec(String pythonCode) {
    * This method will try to launch a python command with error handling
    * 
    * @param gesture
-   *                the gesture
+   *          the gesture
    * @return gesture result
    */
   public String execGesture(String gesture) {
@@ -695,7 +689,7 @@ public void execScript() {
    * a filesystem file :P
    * 
    * @param someScriptName
-   *                       execute a resource script
+   *          execute a resource script
    * @return success or failure
    */
   public void execScript(String someScriptName) {
@@ -773,18 +767,11 @@ public InMoov2Head getHead() {
    */
   public Long getLastActivityTime() {
     Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
-    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime()
-        : null;
-    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime()
-        : null;
-    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null
-        ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime()
-        : null;
-    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null
-        ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime()
-        : null;
-    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime()
-        : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
 
     Long lastActivityTime = null;
 
@@ -951,7 +938,7 @@ public void loadGestures() {
    * file should contain 1 method definition that is the same as the filename.
    * 
    * @param directory
-   *                  - the directory that contains the gesture python files.
+   *          - the directory that contains the gesture python files.
    * @return true/false
    */
   public boolean loadGestures(String directory) {
@@ -1050,8 +1037,7 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D
     moveHand(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1103,10 +1089,8 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring
     moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) {
@@ -1117,10 +1101,8 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin
     moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveTorso(Double topStom, Double midStom, Double lowStom) {
@@ -1149,7 +1131,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
    * comes in from runtime which owns the config list
    * 
    * @param configList
-   *                   list of configs
+   *          list of configs
    */
   public void onConfigList(List<String> configList) {
     this.configList = configList;
@@ -1210,16 +1192,10 @@ public void onJoystickInput(JoystickData input) throws Exception {
    * including lower level logs that do not propegate as statuses
    * 
    * @param log
-   *            - flushed log from Log service
+   *          - flushed log from Log service
    */
-  public void onLogEvents(List<LogEntry> log) {
-    // scan for warn or errors
-    for (LogEntry entry : log) {
-      if ("ERROR".equals(entry.level) && errors.size() < 100) {
-        errors.add(entry);
-        // invoke("publishError", entry);
-      }
-    }
+  public void onErrors(List<LogEntry> log) {
+    errors.addAll(log);
   }
 
   public String onNewState(String state) {
@@ -1559,8 +1535,7 @@ public Heartbeat publishHeartbeat() {
       }
 
       // interval event firing
-      if (config.stateRandomInterval != null
-          && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
         // fsm.fire("random");
         stateLastRandomTime = System.currentTimeMillis();
       }
@@ -1612,8 +1587,7 @@ public Message publishMessage(Message msg) {
     return msg;
   }
 
-  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder,
-      Double omoplate) {
+  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("bicep", bicep);
     map.put("rotate", rotate);
@@ -1627,8 +1601,7 @@ public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double
     return map;
   }
 
-  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure,
-      Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Object> map = new HashMap<>();
     map.put("which", which);
     map.put("thumb", thumb);
@@ -1645,8 +1618,7 @@ public HashMap<String, Object> publishMoveHand(String which, Double thumb, Doubl
     return map;
   }
 
-  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw,
-      Double rollNeck) {
+  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("neck", neck);
     map.put("rothead", rothead);
@@ -1666,8 +1638,7 @@ public HashMap<String, Double> publishMoveLeftArm(Double bicep, Double rotate, D
     return map;
   }
 
-  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1687,8 +1658,7 @@ public HashMap<String, Double> publishMoveRightArm(Double bicep, Double rotate,
     return map;
   }
 
-  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1833,8 +1803,7 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     InMoov2Hand hand = getHand(which);
     if (hand == null) {
       warn("%s hand not started", which);
@@ -1844,14 +1813,12 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1867,8 +1834,7 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null);
   }
 
-  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1892,8 +1858,7 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl
   }
 
   @Deprecated
-  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1905,15 +1870,12 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int
     setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   @Override
@@ -1977,15 +1939,12 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In
     setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public boolean setSpeechType(String speechType) {
@@ -2003,8 +1962,10 @@ public boolean setSpeechType(String speechType) {
     String peerName = getName() + ".mouth";
     Plan plan = runtime.getDefault(peerName, speechType);
     try {
-      SpeechSynthesisConfig mouth = (SpeechSynthesisConfig) plan.get(peerName);
-      mouth.speechRecognizers = new String[] { getName() + ".ear" };
+      // this should be handled in config.listeners
+      // SpeechSynthesisConfig mouth = (SpeechSynthesisConfig)
+      // plan.get(peerName);
+      // mouth.speechRecognizers = new String[] { getName() + ".ear" };
 
       savePeerConfig("mouth", plan.get(peerName));
 
@@ -2155,8 +2116,7 @@ public ProgramAB startChatBot() {
       chatBot.setPredicate("null", "");
       // load last user session
       if (!chatBot.getPredicate("name").isEmpty()) {
-        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown")
-            || chatBot.getPredicate("lastUsername").equals("default")) {
+        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown") || chatBot.getPredicate("lastUsername").equals("default")) {
           chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
         }
       }
@@ -2172,8 +2132,7 @@ public ProgramAB startChatBot() {
       // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
       // chatBot.startSession(chatBot.getPredicate("lastUsername"));
       // }
-      if (chatBot.getPredicate("default", "firstinit").isEmpty()
-          || chatBot.getPredicate("default", "firstinit").equals("unknown")
+      if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown")
           || chatBot.getPredicate("default", "firstinit").equals("started")) {
         chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
         invoke("publishEvent", "FIRST INIT");
@@ -2319,19 +2278,27 @@ public void stopNeopixelAnimation() {
 
   public void systemCheck() {
     Platform platform = Runtime.getPlatform();
-    int servoCount = 0;    
+    int servoCount = 0;
     for (ServiceInterface si : Runtime.getServices()) {
       if (si.getClass().getSimpleName().equals("Servo")) {
         servoCount++;
       }
     }
 
+    // TODO check for latest version if not experimental
+    // TODO change to experimental :)
+    String version = ("unknownVersion".equals(platform.getVersion())) ? "experimental" : platform.getVersion();
+
+    setPredicate("system_version", version);
     setPredicate("system_uptime", Runtime.getUptime());
     setPredicate("system_servo_count", servoCount);
-    setPredicate("system_free_memory", Runtime.getFreeMemory());
-    setPredicate("system_version", platform.getVersion());
-    setPredicate("system_errors", errors.size());
-    
+    setPredicate("system_service_count", Runtime.getServices().size());
+    setPredicate("system_free_memory", Runtime.getFreeMemory() / 1000000);
+    setPredicate("system_errors_exist", errors.size() > 0);
+    setPredicate("system_error_count", errors.size());
+    setPredicate("system_battery_level", Runtime.getBatteryLevel());
+    setPredicate("state", getState());
+
   }
 
   public String systemEvent(String eventMsg) {
diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java
index 16dec730f3..dc20806cf2 100644
--- a/src/main/java/org/myrobotlab/service/Log.java
+++ b/src/main/java/org/myrobotlab/service/Log.java
@@ -132,6 +132,7 @@ public void addError(String msg) {
 
   @Override
   public void addError(String arg0, Throwable arg1) {
+    System.out.println("addError");
   }
 
   @Override
@@ -202,6 +203,15 @@ synchronized public void flush() {
     if (buffer.size() > 0) {
       // bucket add to sliding window
       logs.addAll(buffer);
+      
+      List<LogEntry> errors = new ArrayList<>();
+      for(int i = 0; i < buffer.size(); ++i) {
+        errors.add(buffer.get(i));
+      }
+      if (errors.size() > 0) {
+        invoke("publishErrors", errors);
+      }
+      
       invoke("publishLogEvents", buffer);
       buffer = new ArrayList<>(maxSize);
       lastPublishLogTimeTs = System.currentTimeMillis();
diff --git a/src/main/java/org/myrobotlab/service/OakD.java b/src/main/java/org/myrobotlab/service/OakD.java
index 5ea7b51dca..7918137e37 100644
--- a/src/main/java/org/myrobotlab/service/OakD.java
+++ b/src/main/java/org/myrobotlab/service/OakD.java
@@ -19,8 +19,10 @@
 
 /**
  * 
+ * https://github.com/luxonis/depthai
+ * python3 depthai_demo.py -cb callbacks.py
  * 
- * 
+ * https://github.com/luxonis/depthai-experiments/tree/master/gen2-face-recognition
  * 
  * @author GroG
  *
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 1e884c1e4a..ef14918ac8 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -516,7 +516,9 @@ public Plan getDefault(Plan plan, String name) {
 
     LogConfig log = (LogConfig) plan.get(getPeerName("log"));
     log.level = "WARN";
-    log.listeners.add(new Listener("publishLogEvents", name));
+    log.listeners.add(new Listener("publishErrors", name));
+    // service --to--> InMoov2
+
 
     // mouth_audioFile.listeners.add(new Listener("publishAudioEnd", name));
     // mouth_audioFile.listeners.add(new Listener("publishAudioStart", name));
@@ -542,6 +544,8 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishMoveTorso", getPeerName("torso"), "onMove"));
 
     // service --to--> InMoov2
+    
+    
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
     

From ac9884cdeb9079314c8c7cbcd398e76e1e380b46 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sun, 18 Feb 2024 21:18:45 -0800
Subject: [PATCH 082/131] updates

---
 .../org/myrobotlab/service/AudioFile.java     |  80 ++----
 .../service/FiniteStateMachine.java           |  11 +-
 .../java/org/myrobotlab/service/InMoov2.java  |  13 +-
 .../java/org/myrobotlab/service/NeoPixel.java |   2 +-
 .../java/org/myrobotlab/service/Runtime.java  |   5 -
 .../service/config/AudioFileConfig.java       |   6 +-
 .../config/FiniteStateMachineConfig.java      |   2 +-
 .../service/config/InMoov2Config.java         |   2 +-
 .../WebGui/app/service/js/RuntimeGui.js       |   6 +-
 .../WebGui/app/service/tab-header.html        | 228 +++++++++---------
 .../WebGui/app/service/views/RuntimeGui.html  |   2 +-
 .../WebGui/app/widget/modal-dialog.view.html  |   2 +-
 12 files changed, 153 insertions(+), 206 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java
index dbf2131bcf..39ce2dd345 100644
--- a/src/main/java/org/myrobotlab/service/AudioFile.java
+++ b/src/main/java/org/myrobotlab/service/AudioFile.java
@@ -115,18 +115,13 @@ public class AudioFile extends Service<AudioFileConfig> implements AudioPublishe
   // https://stackoverflow.com/questions/25798200/java-record-mic-to-byte-array-and-play-sound
   //
 
-  String currentTrack = DEFAULT_TRACK;
+  /**
+   * status field, the current track being played
+   */
+  protected String currentTrack = DEFAULT_TRACK;
 
   transient Map<String, AudioProcessor> processors = new HashMap<String, AudioProcessor>();
 
-  double volume = 1.0f;
-  // if set to true, playback will become a no-op
-  private boolean mute = false;
-
-  protected String currentPlaylist = "default";
-
-  protected Map<String, List<String>> playlists = new HashMap<>();
-
   final private transient PlaylistPlayer playlistPlayer = new PlaylistPlayer(this);
 
   public void attach(Attachable attachable) {
@@ -254,7 +249,7 @@ public AudioData playAudioData(AudioData data) {
       data.track = currentTrack;
     }
     setTrack(data.track);
-    processors.get(data.track).setVolume(volume);
+    processors.get(data.track).setVolume(config.volume);
     if (AudioData.MODE_QUEUED.equals(data.mode)) {
       // stick it on top of queue and let our default player play it
       return processors.get(data.track).add(data);
@@ -329,7 +324,7 @@ public void silence() {
    * 
    */
   public void setVolume(float volume) {
-    this.volume = volume;
+    config.volume = volume;
   }
 
   public void setVolume(double volume) {
@@ -337,7 +332,7 @@ public void setVolume(double volume) {
   }
 
   public double getVolume() {
-    return this.volume;
+    return config.volume;
   }
 
   public String getTrack() {
@@ -441,28 +436,28 @@ public void deleteFile(String filename) {
   }
 
   public boolean isMute() {
-    return mute;
+    return config.mute;
   }
 
   public void setMute(boolean mute) {
-    this.mute = mute;
+    config.mute = mute;
   }
 
   public void setPlaylist(String name) {
-    currentPlaylist = name;
+    config.currentPlaylist = name;
   }
 
   public void addPlaylist(String folderPath) {
-    addPlaylist(currentPlaylist, folderPath);
+    addPlaylist(config.currentPlaylist, folderPath);
   }
 
   public void addPlaylist(String name, String path) {
 
     List<String> list = null;
-    if (!playlists.containsKey(name)) {
+    if (!config.playlists.containsKey(name)) {
       list = new ArrayList<String>();
     } else {
-      list = playlists.get(name);
+      list = config.playlists.get(name);
     }
     File check = new File(path);
     if (!check.exists()) {
@@ -473,7 +468,7 @@ public void addPlaylist(String name, String path) {
       list.addAll(scanForMusicFiles(path));
     }
     int filecount = list.size();
-    playlists.put(name, list);
+    config.playlists.put(name, list);
     log.info("{} playlist added {} files", name, filecount);
   }
 
@@ -505,15 +500,15 @@ private List<String> scanForMusicFiles(String path) {
   }
 
   public List<String> getPlaylist(String name) {
-    return playlists.get(name);
+    return config.playlists.get(name);
   }
 
   public Map<String, List<String>> getPlaylists() {
-    return playlists;
+    return config.playlists;
   }
 
   public void startPlaylist() {
-    startPlaylist(currentPlaylist, false, false, currentPlaylist);
+    startPlaylist(config.currentPlaylist, false, false, DEFAULT_TRACK);
   }
 
   public void startPlaylist(String playlist) {
@@ -525,54 +520,17 @@ public void startPlaylist(String playlist, boolean shuffle, boolean repeat) {
   }
 
   public void startPlaylist(String playlist, boolean shuffle, boolean repeat, String track) {
-    if (!playlists.containsKey(playlist)) {
+    if (!config.playlists.containsKey(playlist)) {
       error("cannot play playlist %s does not exists", playlist);
       return;
     }
-    playlistPlayer.start(playlists.get(playlist), shuffle, repeat, track);
+    playlistPlayer.start(config.playlists.get(playlist), shuffle, repeat, track);
   }
 
   public void stopPlaylist() {
     playlistPlayer.stop();
   }
 
-  @Override
-  public AudioFileConfig getConfig() {
-
-    AudioFileConfig c = (AudioFileConfig) super.getConfig();
-    // FIXME - remove members keep data in config !
-    // FIXME - the following is not needed nor desired
-    // useless self assignment
-    c.mute = mute;
-    c.currentTrack = currentTrack;
-    c.currentPlaylist = currentPlaylist;
-    // c.peakMultiplier = peakMultiplier;
-    c.volume = volume;
-    c.playlists = playlists;
-    // config.peakSampleInterval <- this one is done correctly no maintenance
-    c.audioListeners = getAttached("publishAudio").toArray(new String[0]);
-
-    return config;
-  }
-
-  public AudioFileConfig apply(AudioFileConfig config) {
-    super.apply(config);
-    setMute(config.mute);
-    setTrack(config.currentTrack);
-    setVolume(config.volume);
-    setPlaylist(config.currentPlaylist);
-    if (config.playlists != null) {
-      playlists = config.playlists;
-    }
-
-    if (config.audioListeners != null) {
-      for (String listener : config.audioListeners) {
-        attachAudioListener(listener);
-      }
-    }
-    
-    return config;
-  }
 
   public double publishPeak(double peak) {
     log.debug("publishPeak {}", peak);
diff --git a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
index e998711e26..5ab6cf5604 100644
--- a/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
+++ b/src/main/java/org/myrobotlab/service/FiniteStateMachine.java
@@ -254,13 +254,6 @@ public StateChange publishStateChange(StateChange stateChange) {
     return stateChange;
   }
 
-  @Override
-  public FiniteStateMachineConfig getConfig() {
-    super.getConfig();
-    config.current = getState();
-    return config;
-  }
-
   @Override
   public FiniteStateMachineConfig apply(FiniteStateMachineConfig c) {
     super.apply(c);
@@ -280,8 +273,8 @@ public FiniteStateMachineConfig apply(FiniteStateMachineConfig c) {
     }
 
     // setCurrent
-    if (c.current != null) {
-      setCurrent(c.current);
+    if (c.start != null) {
+      setCurrent(c.start);
     }
 
     return c;
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index b83c909e47..be0f976919 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -102,7 +102,6 @@ public static class Heartbeat {
     double batteryLevel = 100;
     public long count = 0;
     public List<LogEntry> errors;
-    public boolean isPirOn = false;
     public String state;
     public long ts = System.currentTimeMillis();
 
@@ -110,7 +109,6 @@ public Heartbeat(InMoov2 inmoov) {
       this.state = inmoov.state;
       this.errors = inmoov.errors;
       this.count = inmoov.heartbeatCount;
-      this.isPirOn = inmoov.isPirOn;
     }
   }
 
@@ -237,8 +235,6 @@ public static void main(String[] args) {
 
   protected transient ImageDisplay imageDisplay;
 
-  protected boolean isPirOn = false;
-
   protected boolean isSpeaking = false;
 
   protected String lastGestureExecuted;
@@ -1239,16 +1235,12 @@ public void onPeak(double volume) {
    */
   public void onPirOn() {
     log.info("onPirOn");
-    // FIXME flash on config.flashOnBoot
-    invoke("publishFlash", "pir");
-    String botState = chatBot.getPredicate("botState");
-    if ("sleeping".equals(botState)) {
-      invoke("publishEvent", "WAKE");
-    }
+    processMessage("onPirOn");
   }
 
   public void onPirOff() {
     log.info("onPirOff");
+    processMessage("onPirOff");
   }
 
   // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python
@@ -1682,6 +1674,7 @@ public String publishPlayAudioFile(String filename) {
   }
 
   /**
+   * One of the most important publishing point.
    * Processing publishing point, where everything InMoov2 wants to be processed
    * is turned into a message and published.
    * 
diff --git a/src/main/java/org/myrobotlab/service/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java
index a5a22c4782..a81397f49b 100644
--- a/src/main/java/org/myrobotlab/service/NeoPixel.java
+++ b/src/main/java/org/myrobotlab/service/NeoPixel.java
@@ -937,7 +937,7 @@ public void setPixel(String matrixName, Integer pixelSetIndex, int address, int
     // Runtime.getService(controller);
     ServiceInterface sc = Runtime.getService(controller);
     if (sc == null) {
-      error("controler %s not valid", controller);
+      error("controller %s not valid", controller);
       return;
     }
 
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 9b76d3bf65..d8a020dd84 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -433,11 +433,6 @@ private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, M
       RuntimeConfig currentConfig = Runtime.getInstance().config;
 
       for (String service : plansRtConfig.getRegistry()) {
-        // FIXME - determine if you want to return a complete merge of activated
-        // or just "recent"
-        if (Runtime.getService(service) != null) {
-          continue;
-        }
         ServiceConfig sc = plan.get(service);
         if (sc == null) {
           runtime.error("could not get %s from plan", service);
diff --git a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
index 391d175d55..086e93691f 100644
--- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
@@ -3,12 +3,16 @@
 import java.util.List;
 import java.util.Map;
 
+import org.myrobotlab.service.AudioFile;
+
 public class AudioFileConfig extends ServiceConfig {
 
   public boolean mute = false;
-  public String currentTrack = "default";
+
   public double volume = 1.0;
+  
   public String currentPlaylist = "default";
+  
   public Map<String, List<String>> playlists;
   
   @Deprecated /* use regular "listeners" from ServiceConfig parent */
diff --git a/src/main/java/org/myrobotlab/service/config/FiniteStateMachineConfig.java b/src/main/java/org/myrobotlab/service/config/FiniteStateMachineConfig.java
index bd4e5648f3..e69c44a4f7 100644
--- a/src/main/java/org/myrobotlab/service/config/FiniteStateMachineConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/FiniteStateMachineConfig.java
@@ -23,7 +23,7 @@ public Transition(String from, String event, String to) {
 
   public List<Transition> transitions = new ArrayList<>();
 
-  public String current = null;
+  public String start = null;
   
 
 }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index ef14918ac8..49575f58c4 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -369,7 +369,7 @@ public Plan getDefault(Plan plan, String name) {
     // TODO - events easily gotten from InMoov data ?? auto callbacks in python
     // if
     // exists ?
-    fsm.current = "boot";
+    fsm.start = "boot";
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
     // setup, nor sleep should be affected by idle
     fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
diff --git a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
index fbb2edc05d..b1007dcbc1 100644
--- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js
@@ -388,14 +388,14 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$
 
           modalInstance.result.then(function(result) {
             // Handle 'OK' button click
-            console.log('Config Name: ' + $scope.service.configName)
+            console.log('Config Name: ' + $scope.configName)
             console.log('Selected Option: ' + $scope.service.selectedOption)
             console.log('includePeers Option: ' + $scope.service.includePeers)
             console.log('configType Option: ' + $scope.service.configType)
             if ($scope.service.selectedOption == 'default'){
-                msg.send('saveDefault', $scope.service.configName, $scope.service.defaultServiceName, $scope.service.configType, $scope.service.includePeers)
+                msg.send('saveDefault', $scope.configName, $scope.service.defaultServiceName, $scope.service.configType, $scope.service.includePeers)
             } else {
-                msg.sendTo('runtime', 'saveConfig', $scope.service.configName)
+                msg.sendTo('runtime', 'saveConfig', $scope.configName)
             }
           }, function() {
             // Handle 'Cancel' button click or modal dismissal
diff --git a/src/main/resources/resource/WebGui/app/service/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html
index 4abedeb490..a87ea4c8cf 100644
--- a/src/main/resources/resource/WebGui/app/service/tab-header.html
+++ b/src/main/resources/resource/WebGui/app/service/tab-header.html
@@ -1,117 +1,121 @@
 <div class="tab-header col-md-12">
-    <table>
+  <table>
+    <tr>
+      <td valign="top">
+        <button class="btn btn-default" ng-click="mrl.goBack()" title="back">
+          <span class="glyphicon glyphicon-arrow-left"></span>
+        </button>
+      </td>
+      <td>
+        <div class="dropdown">
+          <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
+            <img ng-src="{{::service.simpleName}}.png" alt="" width="16" />
+            &nbsp;&nbsp {{::service.simpleName}}&nbsp;&nbsp;{{::service.name}}@{{::service.id}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{::service.serviceVersion}}
+            <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu">
+            <li class="dropdown-header">service functions</li>
+            <li>
+              <a href="http://myrobotlab.org/service/{{::service.simpleName}}" target="_blank" ng-click="servicemenuDropdownOpen = false">
+                <i class="glyphicon glyphicon-question-sign"></i>
+                &nbsp;&nbsp;help
+              </a>
+            </li>
+            <li>
+              <a href="" ng-click="servicemenuDropdownOpen = false;msg.broadcastState()">
+                <i class="glyphicon glyphicon-refresh"></i>
+                &nbsp;&nbsp;refresh
+              </a>
+            </li>
+            <li>
+              <a href="" target="_blank" width="100%">
+                <i class="glyphicon glyphicon-equalizer"></i>
+                <span ng-show="!parentPanel.showPeerTable" ng-click="showPeers(true)">&nbsp;&nbsp;show peers</span>
+                <span ng-show="parentPanel.showPeerTable" ng-click="showPeers(false)">&nbsp;&nbsp;hide peers</span>
+              </a>
+            </li>
+            <li class="divider"></li>
+            <li class="dropdown-header">json</li>
+            <li>
+              <!-- a href="" target="_blank" ng-click="showProperties=!showProperties" -->
+              <a href="/api/service/{{::service.name}}" target="_blank">
+                <i class="glyphicon glyphicon-list-alt"></i>
+                &nbsp;&nbsp;properties
+              </a>
+            </li>
+            <li>
+              <!-- a href="" target="_blank" ng-click="showMethods=!showMethods" -->
+              <a href="/api/service/{{::service.name}}/" target="_blank">
+                <i class="glyphicon glyphicon-list-alt"></i>
+                &nbsp;&nbsp;methods
+              </a>
+            </li>
+            <li>
+              <!-- a href="" target="_blank" ng-click="showMethods=!showMethods" -->
+              <a href='/api/service/runtime/getNotifyEntries/"{{service.name}}"' target="_blank">
+                <i class="glyphicon glyphicon-list-alt"></i>
+                &nbsp;&nbsp;subscriptions
+              </a>
+            </li>
+          </ul>
+        </div>
+      </td>
+      <td>
+        <button class="btn btn-default" ng-click="release()" title="stops and releases a service">
+          <span class="glyphicon glyphicon-remove red"></span>
+        </button>
+      </td>
+      <td>
+        <button class="btn btn-default" ng-click="save()" title="save current configuration">
+          <span class="glyphicon glyphicon-save-file"></span>
+        </button>
+      </td>
+      <td>
+        <button class="btn btn-default" ng-click="apply()" title="load and apply configuration">
+          <span class="glyphicon glyphicon-open-file"></span>
+        </button>
+      </td>
+    </tr>
+  </table>
+  <div ng-show="parentPanel.showPeerTable">
+    <table class="table table-sm">
+      <thead>
         <tr>
-            <td valign="top">
-                <button class="btn btn-default" ng-click="mrl.goBack()" title="back">
-                    <span class="glyphicon glyphicon-arrow-left"></span>
-                </button>
-            </td>
-            <td>
-                <div class="dropdown">
-                    <button class="btn btn-default  dropdown-toggle" type="button" data-toggle="dropdown">
-                        <img ng-src="{{::service.simpleName}}.png" alt="" width="16">
-                        &nbsp;&nbsp {{::service.simpleName}}&nbsp;&nbsp;{{::service.name}}@{{::service.id}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{::service.serviceVersion}}<span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu">
-                        <li class="dropdown-header">service functions</li>
-                        <li>
-                            <a href="http://myrobotlab.org/service/{{::service.simpleName}}" target="_blank" ng-click="servicemenuDropdownOpen = false">
-                                <i class="glyphicon glyphicon-question-sign"></i>
-                                &nbsp;&nbsp;help
-                            </a>
-                        </li>
-                        <li>
-                            <a href="" ng-click="servicemenuDropdownOpen = false;msg.broadcastState()">
-                                <i class="glyphicon glyphicon-refresh"></i>
-                                &nbsp;&nbsp;refresh
-                            </a>
-                        </li>
-                        <li>
-                            <a href="" target="_blank" width="100%">
-                                <i class="glyphicon glyphicon-equalizer"></i>
-                                <span ng-show="!parentPanel.showPeerTable" ng-click="showPeers(true)">&nbsp;&nbsp;show peers</span>
-                                <span ng-show="parentPanel.showPeerTable" ng-click="showPeers(false)">&nbsp;&nbsp;hide peers</span>
-                            </a>
-                        </li>
-                        <li class="divider"></li>
-                        <li class="dropdown-header">json</li>
-                        <li>
-                            <!-- a href="" target="_blank" ng-click="showProperties=!showProperties" -->
-                            <a href="/api/service/{{::service.name}}" target="_blank">
-                                <i class="glyphicon glyphicon-list-alt"></i>
-                                &nbsp;&nbsp;properties
-                            
-                            
-                            </a>
-                        </li>
-                        <li>
-                            <!-- a href="" target="_blank" ng-click="showMethods=!showMethods" -->
-                            <a href="/api/service/{{::service.name}}/" target="_blank">
-                                <i class="glyphicon glyphicon-list-alt"></i>
-                                &nbsp;&nbsp;methods
-                            
-                            
-                            </a>
-                        </li>
-                        <li>
-                            <!-- a href="" target="_blank" ng-click="showMethods=!showMethods" -->
-                            <a href="/api/service/runtime/getNotifyEntries/{{service.name}}" target="_blank">
-                                <i class="glyphicon glyphicon-list-alt"></i>
-                                &nbsp;&nbsp;subscriptions
-                            </a>
-                        </li>
-                    </ul>
-                </div>
-            </td>
-            <td>
-                <button class="btn btn-default" ng-click="release()" title="stops and releases a service">
-                    <span class="glyphicon glyphicon-remove red"></span>
-                </button>
-            </td>
-            <td>
-                <button class="btn btn-default" ng-click="save()" title="save current configuration">
-                    <span class="glyphicon glyphicon-save-file"></span>
-                </button>
-            </td>
-            <td>
-                <button class="btn btn-default" ng-click="apply()" title="load and apply configuration">
-                    <span class="glyphicon glyphicon-open-file"></span>
-                </button>
-            </td>
-
+          <th scope="col">key</th>
+          <th scope="col">name</th>
+          <th scope="col">state</th>
+          <th scope="col"></th>
         </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="(key, value) in service.config.peers">
+          <td>
+            <span>
+              <a href="" ng-click="mrl.changeTab(peer.getActualName(service, key))">
+                <img width="32" ng-src="/{{value.type + '.png'}}" width="48" />
+              </a>
+              {{peer.getActualName(service, key)}}
+            </span>
+          </td>
+          <td>
+            {{value.key}}
+            <br />
+            {{value.type}}
+          </td>
+          <td>{{value.state}}</td>
+          <td>
+            <toggle
+              width="30"
+              height="28"
+              ng-model="service['is' + key[0].toUpperCase() + key.substring(1) + 'Started']"
+              ng-change="service['is' + key[0].toUpperCase() + key.substring(1) + 'Started']?startPeer(key):releasePeer(key)"
+              on=""
+              off=""
+            />
+          </td>
+          <td></td>
+        </tr>
+      </tbody>
     </table>
-    <div ng-show="parentPanel.showPeerTable">
-        <table class="table table-sm">
-            <thead>
-                <tr>
-                    <th scope="col">key</th>
-                    <th scope="col">name</th>
-                    <th scope="col">state</th>
-                    <th scope="col"></th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr ng-repeat="(key, value) in service.config.peers">
-                    <td>
-                        <span>
-
-                            <a href=""  ng-click="mrl.changeTab(peer.getActualName(service, key))">
-                                <img width="32" ng-src="/{{value.type + '.png'}}" width="48">
-                            </a>
-                                    {{peer.getActualName(service, key)}}
-                        </span>
-                    </td>
-                    <td>
-                        {{value.key}}<br/>{{value.type}}
-                    </td>
-                    <td>{{value.state}}</td>
-                    <td>
-                        <toggle width="30" height="28" ng-model="service['is' + key[0].toUpperCase() + key.substring(1) + 'Started']" ng-change="service['is' + key[0].toUpperCase() + key.substring(1) + 'Started']?startPeer(key):releasePeer(key)" on="" off=""/>
-                    </td>
-                    <td></td>
-                </tr>
-            </tbody>
-        </table>
-    </div>
+  </div>
 </div>
diff --git a/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html b/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html
index 7e1fd406ff..52229335c0 100644
--- a/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/RuntimeGui.html
@@ -153,7 +153,7 @@ <h4 class="modal-title">Save Configuration</h4>
       <tr>
         <td>
           Save your current configuration in a directory named<br/>
-          <input type="text" ng-model="service.configName" class="form-control" placeholder="Configuration Name">
+          <input type="text" ng-model="configName" class="form-control" placeholder="Configuration Name">
           </td>
           <td><br/><br/>
             <input ng-show="service.selectedOption==='default'" type="text" ng-model="service.defaultServiceName" class="form-control" placeholder="Service Name">
diff --git a/src/main/resources/resource/WebGui/app/widget/modal-dialog.view.html b/src/main/resources/resource/WebGui/app/widget/modal-dialog.view.html
index f8f7104400..38343da431 100644
--- a/src/main/resources/resource/WebGui/app/widget/modal-dialog.view.html
+++ b/src/main/resources/resource/WebGui/app/widget/modal-dialog.view.html
@@ -7,7 +7,7 @@ <h4 class="modal-title" id="myModalLabel">{{title }}</h4>
 
         <div class="form-inline">
             <div class="form-group">
-                <input type="text" class="form-control" ng-model="service.configName" placeholder="myconfig">
+                <input type="text" class="form-control" ng-model="selectedConfig" placeholder="myconfig">
             </div>
         </div>
 

From a63e083a27996cd3db67d96873b24472e5950ab4 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 19 Feb 2024 07:35:57 -0800
Subject: [PATCH 083/131] removed adding system tray icon

---
 src/main/java/org/myrobotlab/service/ImageDisplay.java | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/main/java/org/myrobotlab/service/ImageDisplay.java b/src/main/java/org/myrobotlab/service/ImageDisplay.java
index d921e2f270..4be2abae11 100644
--- a/src/main/java/org/myrobotlab/service/ImageDisplay.java
+++ b/src/main/java/org/myrobotlab/service/ImageDisplay.java
@@ -275,6 +275,7 @@ public void run() {
 
           // TODO - make better / don't use setImageAutoSize (very bad
           // algorithm)
+          /** <pre> No real use, and doesn't remove
           if (SystemTray.isSupported()) {
             log.info("SystemTray is supported");
             SystemTray tray = SystemTray.getSystemTray();
@@ -285,6 +286,8 @@ public void run() {
 
             tray.add(trayIcon);
           }
+          </pre>
+          */
 
           if (display.bgColor != null) {
             Color color = Color.decode(display.bgColor);

From 98a24286150d5668139a7b92d1027b3d9cc96c88 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 19 Feb 2024 07:36:58 -0800
Subject: [PATCH 084/131] removed onPeak info

---
 src/main/java/org/myrobotlab/service/InMoov2.java | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index be0f976919..baa89cae49 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -1222,11 +1222,7 @@ public OpenCVData onOpenCVData(OpenCVData data) {
    * @param volume
    */
   public void onPeak(double volume) {
-    if (config.neoPixelFlashWhenSpeaking && !"boot".equals(getState())) {
-      if (volume > 0.5) {
-        invoke("publishSpeakingFlash", "speaking");
-      }
-    }
+    processMessage("onPeak", volume);
   }
 
   /**

From 2b5823df6e35a553573e5f360984614d8280b097 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Mon, 19 Feb 2024 21:02:29 -0800
Subject: [PATCH 085/131] audiofile config playlist init

---
 .../java/org/myrobotlab/service/config/AudioFileConfig.java  | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
index 086e93691f..47b3dc9b91 100644
--- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
@@ -2,8 +2,7 @@
 
 import java.util.List;
 import java.util.Map;
-
-import org.myrobotlab.service.AudioFile;
+import java.util.TreeMap;
 
 public class AudioFileConfig extends ServiceConfig {
 
@@ -13,7 +12,7 @@ public class AudioFileConfig extends ServiceConfig {
   
   public String currentPlaylist = "default";
   
-  public Map<String, List<String>> playlists;
+  public Map<String, List<String>> playlists = new TreeMap<>();
   
   @Deprecated /* use regular "listeners" from ServiceConfig parent */
   public String[] audioListeners;

From 72da4c5f1baa17b4e7410911e944702969834fe7 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 20 Feb 2024 09:14:32 -0800
Subject: [PATCH 086/131] updates

---
 .../org/myrobotlab/framework/CmdOptions.java  |   2 +-
 .../java/org/myrobotlab/service/InMoov2.java  | 337 +++++++-----------
 .../org/myrobotlab/service/ProgramAB.java     |  15 +-
 .../java/org/myrobotlab/service/Random.java   |  14 +-
 .../service/config/InMoov2Config.java         |   5 +-
 .../org/myrobotlab/service/HarryTest.java     |   2 +-
 6 files changed, 153 insertions(+), 222 deletions(-)

diff --git a/src/main/java/org/myrobotlab/framework/CmdOptions.java b/src/main/java/org/myrobotlab/framework/CmdOptions.java
index 2c357e8db6..3b3b18d7bb 100644
--- a/src/main/java/org/myrobotlab/framework/CmdOptions.java
+++ b/src/main/java/org/myrobotlab/framework/CmdOptions.java
@@ -39,7 +39,7 @@ static boolean contains(List<String> l, String flag) {
 
   // launcher
   @Option(names = { "-c",
-      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
+      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config my-config-dir to start the configuration stored in config data/config/my-config-dir")
   public String config = null;
 
   @Option(names = { "-h", "-?", "--help" }, description = "shows help")
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index baa89cae49..2322a4bc8e 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -3,17 +3,16 @@
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -33,6 +32,7 @@
 import org.myrobotlab.opencv.OpenCVData;
 import org.myrobotlab.programab.PredicateEvent;
 import org.myrobotlab.programab.Response;
+import org.myrobotlab.programab.Session;
 import org.myrobotlab.service.FiniteStateMachine.StateChange;
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
@@ -47,7 +47,6 @@
 import org.myrobotlab.service.interfaces.Simulator;
 import org.myrobotlab.service.interfaces.SpeechListener;
 import org.myrobotlab.service.interfaces.SpeechRecognizer;
-import org.myrobotlab.service.interfaces.SpeechSynthesis;
 import org.myrobotlab.service.interfaces.TextListener;
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
@@ -114,12 +113,8 @@ public Heartbeat(InMoov2 inmoov) {
 
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
-  public static LinkedHashMap<String, String> lpVars = new LinkedHashMap<String, String>();
-
   private static final long serialVersionUID = 1L;
 
-  static String speechRecognizer = "WebkitSpeechRecognition";
-
   /**
    * This method will load a python file into the python interpreter.
    * 
@@ -187,8 +182,6 @@ public static void main(String[] args) {
     }
   }
 
-  protected Double batteryLevel = 100.0;
-
   /**
    * number of times waited in boot state
    */
@@ -198,11 +191,6 @@ public static void main(String[] args) {
 
   protected List<String> configList;
 
-  /**
-   * map of events or states to sounds
-   */
-  protected Map<String, String> customSoundMap = new TreeMap<>();
-
   protected transient SpeechRecognizer ear;
 
   protected List<LogEntry> errors = new ArrayList<>();
@@ -213,7 +201,7 @@ public static void main(String[] args) {
    * there will be a direct reference to the fsm. If different state graph is
    * needed, then the fsm can provide that service.
    */
-  private transient FiniteStateMachine fsm = null;
+  private transient FiniteStateMachine fsm;
 
   // waiting controable threaded gestures we warn user
   protected boolean gestureAlreadyStarted = false;
@@ -223,24 +211,18 @@ public static void main(String[] args) {
   /**
    * Prevents actions or events from happening when InMoov2 is first booted
    */
-  private boolean hasBooted = false;
+  protected boolean hasBooted = false;
 
   private transient final Heart heart = new Heart();
 
   protected long heartbeatCount = 0;
 
-  protected boolean heartBeating = false;
-
-  protected transient HtmlFilter htmlFilter;
-
   protected transient ImageDisplay imageDisplay;
 
   protected boolean isSpeaking = false;
 
   protected String lastGestureExecuted;
 
-  protected Long lastPirActivityTime;
-
   protected String lastState = null;
 
   /**
@@ -250,8 +232,6 @@ public static void main(String[] args) {
 
   protected int maxInactivityTimeSeconds = 120;
 
-  protected transient SpeechSynthesis mouth;
-
   protected boolean mute = false;
 
   protected transient OpenCV opencv;
@@ -261,7 +241,7 @@ public static void main(String[] args) {
   /**
    * initial state - updated on any state change
    */
-  String state = "boot";
+  protected String state = "boot";
 
   protected long stateLastIdleTime = System.currentTimeMillis();
 
@@ -292,6 +272,8 @@ public InMoov2Config apply(InMoov2Config c) {
       } else {
         setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString()));
       }
+      // one way sync configuration into predicates
+      configToPredicates();
 
     } catch (Exception e) {
       error(e);
@@ -567,6 +549,13 @@ public long checkInactivity() {
     return lastActivityTime;
   }
 
+  /**
+   * clear all errors
+   */
+  public void clearErrors() {
+    errors.clear();
+  }
+
   public void closeAllImages() {
     // FIXME - follow this pattern ?
     // CON npe possible although unlikely
@@ -576,6 +565,38 @@ public void closeAllImages() {
     imageDisplay.closeAll();
   }
 
+  /**
+   * Updates configuration into ProgramAB predicates.
+   */
+  public void configToPredicates() {
+    log.info("configToPredicates");
+    if (chatBot != null) {
+      Class<?> pojoClass = config.getClass();
+      Field[] fields = pojoClass.getDeclaredFields();
+      for (Field field : fields) {
+        try {
+          field.setAccessible(true);
+          Object value = field.get(config); // Requires handling
+          Map<String, Session> sessions = chatBot.getSessions();
+          if (sessions != null) {
+            for (Session session : sessions.values()) {
+              if (value != null) {
+                session.setPredicate(field.getName(), value.toString());
+              } else {
+                session.setPredicate(field.getName(), null);
+              }
+
+            }
+          }
+        } catch (Exception e) {
+          error(e);
+        }
+      }
+    } else {
+      log.info("chatbot not ready for config sync");
+    }
+  }
+
   public void cycleGestures() {
     // if not loaded load -
     // FIXME - this needs alot of "help" :P
@@ -872,10 +893,6 @@ public InMoov2Torso getTorso() {
     return (InMoov2Torso) getPeer("torso");
   }
 
-  public InMoov2Config getTypedConfig() {
-    return (InMoov2Config) config;
-  }
-
   public void halfSpeed() {
     sendToPeer("head", "setSpeed", 25.0, 25.0, 25.0, 25.0, 100.0, 25.0);
     sendToPeer("rightHand", "setSpeed", 30.0, 30.0, 30.0, 30.0, 30.0, 30.0);
@@ -894,13 +911,6 @@ public boolean hasErrors() {
     return errors.size() > 0;
   }
 
-  /**
-   * clear all errors
-   */
-  public void clearErrors() {
-    errors.clear();
-  }
-
   public boolean isCameraOn() {
     if (opencv != null) {
       if (opencv.isCapturing()) {
@@ -914,6 +924,10 @@ public boolean isMute() {
     return mute;
   }
 
+  public boolean isSpeaking() {
+    return isSpeaking;
+  }
+
   /**
    * execute python scripts in the app directory on startup of the service
    * 
@@ -1144,6 +1158,17 @@ public void onEndSpeaking(String utterance) {
     isSpeaking = false;
   }
 
+  /**
+   * Centralized logging system will have all logging from all services,
+   * including lower level logs that do not propegate as statuses
+   * 
+   * @param log
+   *          - flushed log from Log service
+   */
+  public void onErrors(List<LogEntry> log) {
+    errors.addAll(log);
+  }
+
   public void onFinishedConfig(String configName) {
     log.info("onFinishedConfig");
     // invoke("publishEvent", "configFinished");
@@ -1183,17 +1208,6 @@ public void onJoystickInput(JoystickData input) throws Exception {
     invoke("publishEvent", "joystick");
   }
 
-  /**
-   * Centralized logging system will have all logging from all services,
-   * including lower level logs that do not propegate as statuses
-   * 
-   * @param log
-   *          - flushed log from Log service
-   */
-  public void onErrors(List<LogEntry> log) {
-    errors.addAll(log);
-  }
-
   public String onNewState(String state) {
     log.error("onNewState {}", state);
 
@@ -1225,20 +1239,22 @@ public void onPeak(double volume) {
     processMessage("onPeak", volume);
   }
 
+  public void onPirOff() {
+    log.info("onPirOff");
+    setPredicate(String.format("%s.pir_off", getName()), System.currentTimeMillis());
+    processMessage("onPirOff");
+  }
+
   /**
    * initial callback for Pir sensor Default behavior will be: send fsm event
    * onPirOn flash neopixel
    */
   public void onPirOn() {
     log.info("onPirOn");
+    setPredicate(String.format("%s.pir_on", getName()), System.currentTimeMillis());
     processMessage("onPirOn");
   }
 
-  public void onPirOff() {
-    log.info("onPirOff");
-    processMessage("onPirOff");
-  }
-
   // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python
   // yet provides a stable default, which can be fully replaced
   // Works using common pub/sub rules
@@ -1277,6 +1293,15 @@ public boolean onSense(boolean b) {
     return b;
   }
 
+  /**
+   * When a new session is started this will sync config with it
+   * 
+   * @param sessionKey
+   */
+  public void onSession(String sessionKey) {
+    configToPredicates();
+  }
+
   /**
    * runtime re-publish relay
    * 
@@ -1318,42 +1343,6 @@ public void onStartSpeaking(String utterance) {
     isSpeaking = true;
   }
 
-  /**
-   * publishStateChange
-   * 
-   * The integration between the FiniteStateMachine (fsm) and the InMoov2
-   * service and potentially other services (Python, ProgramAB) happens here.
-   * 
-   * After boot all state changes get published here.
-   * 
-   * Some InMoov2 service methods will be called here for "default
-   * implemenation" of states. If a user doesn't want to have that default
-   * implementation, they can change it by changing the definition of the state
-   * machine, and have a new state which will call a Python inmoov2 library
-   * callback. Overriding, appending, or completely transforming the behavior is
-   * all easily accomplished by managing the fsm and python inmoov2 library
-   * callbacks.
-   * 
-   * Python inmoov2 callbacks ProgramAB topic switching
-   * 
-   * Depending on config:
-   * 
-   * @param stateChange
-   * @return
-   */
-  public StateChange publishStateChange(StateChange stateChange) {
-    log.info("publishStateChange {}", stateChange);
-
-    log.info("onStateChange {}", stateChange);
-
-    lastState = state;
-    state = stateChange.state;
-
-    processMessage("onStateChange", stateChange);
-
-    return stateChange;
-  }
-
   @Override
   public void onStopped(String name) {
     log.info("service {} has stopped");
@@ -1670,9 +1659,9 @@ public String publishPlayAudioFile(String filename) {
   }
 
   /**
-   * One of the most important publishing point.
-   * Processing publishing point, where everything InMoov2 wants to be processed
-   * is turned into a message and published.
+   * One of the most important publishing point. Processing publishing point,
+   * where everything InMoov2 wants to be processed is turned into a message and
+   * published.
    * 
    * @param msg
    * @return
@@ -1702,6 +1691,42 @@ public String publishSpeakingFlash(String name) {
     return name;
   }
 
+  /**
+   * publishStateChange
+   * 
+   * The integration between the FiniteStateMachine (fsm) and the InMoov2
+   * service and potentially other services (Python, ProgramAB) happens here.
+   * 
+   * After boot all state changes get published here.
+   * 
+   * Some InMoov2 service methods will be called here for "default
+   * implemenation" of states. If a user doesn't want to have that default
+   * implementation, they can change it by changing the definition of the state
+   * machine, and have a new state which will call a Python inmoov2 library
+   * callback. Overriding, appending, or completely transforming the behavior is
+   * all easily accomplished by managing the fsm and python inmoov2 library
+   * callbacks.
+   * 
+   * Python inmoov2 callbacks ProgramAB topic switching
+   * 
+   * Depending on config:
+   * 
+   * @param stateChange
+   * @return
+   */
+  public StateChange publishStateChange(StateChange stateChange) {
+    log.info("publishStateChange {}", stateChange);
+
+    log.info("onStateChange {}", stateChange);
+
+    lastState = state;
+    state = stateChange.state;
+
+    processMessage("onStateChange", stateChange);
+
+    return stateChange;
+  }
+
   /**
    * stop animation event
    */
@@ -1788,6 +1813,12 @@ public void setAutoDisable(Boolean param) {
     sendToPeer("torso", "setAutoDisable", param);
   }
 
+  @Override
+  public void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+    super.setConfigValue(fieldname, value);
+    setPredicate(fieldname, value);
+  }
+
   public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
@@ -1902,15 +1933,6 @@ public void setNeopixelAnimation(String animation, Integer red, Integer green, I
     sendToPeer("neopixel", "animation", red, green, blue, speed);
   }
 
-  public void setOpenCV(OpenCV opencv) {
-    this.opencv = opencv;
-  }
-
-  public boolean setPirPlaySounds(boolean b) {
-    getTypedConfig().pirPlaySounds = b;
-    return b;
-  }
-
   public Object setPredicate(String key, Object data) {
     if (data == null) {
       chatBot.setPredicate(key, null); // "unknown" "null" other sillyness ?
@@ -1987,24 +2009,6 @@ public void setTorsoSpeed(Integer topStom, Integer midStom, Integer lowStom) {
     setTorsoSpeed((double) topStom, (double) midStom, (double) lowStom);
   }
 
-  // -----------------------------------------------------------------------------
-  // These are methods added that were in InMoov1 that we no longer had in
-  // InMoov2.
-  // From original InMoov1 so we don't loose the
-
-  @Deprecated /* use setTorsoSpeed */
-  public void setTorsoVelocity(Double topStom, Double midStom, Double lowStom) {
-    setTorsoSpeed(topStom, midStom, lowStom);
-  }
-
-  public void setVoice(String name) {
-    if (mouth != null) {
-      mouth.setVoice(name);
-      voiceSelected = name;
-      speakBlocking(String.format("%s %s", get("SETLANG"), name));
-    }
-  }
-
   public void sleeping() {
     log.error("sleeping");
   }
@@ -2051,103 +2055,6 @@ public void speakBlocking(String format, Object... args) {
     }
   }
 
-  @Deprecated /* use startPeers */
-  public void startAll() throws Exception {
-    startAll(null, null);
-  }
-
-  @Deprecated /* use startPeers */
-  public void startAll(String leftPort, String rightPort) throws Exception {
-    startChatBot();
-
-    // startHeadTracking();
-    // startEyesTracking();
-    // startOpenCV();
-    startEar();
-
-    startServos();
-    // startMouthControl(head.jaw, mouth);
-
-    speakBlocking(get("STARTINGSEQUENCE"));
-  }
-
-  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
-  public void startBrain() {
-    startChatBot();
-  }
-
-  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
-  public ProgramAB startChatBot() {
-
-    try {
-
-      if (locale != null) {
-        chatBot.setCurrentBotName(locale.getTag());
-      }
-
-      // FIXME remove get en.properties stuff
-      speakBlocking(get("CHATBOTACTIVATED"));
-
-      chatBot.attachTextPublisher(ear);
-
-      // this.attach(chatBot); FIXME - attach as a TextPublisher - then
-      // re-publish
-      // FIXME - deal with language
-      // speakBlocking(get("CHATBOTACTIVATED"));
-      chatBot.repetitionCount(10);
-      // chatBot.setPath(getResourceDir() + fs + "chatbot");
-      // chatBot.setPath(getDataDir() + "ProgramAB");
-      chatBot.startSession("default", locale.getTag());
-      // reset some parameters to default...
-      chatBot.setPredicate("topic", "default");
-      chatBot.setPredicate("questionfirstinit", "");
-      chatBot.setPredicate("tmpname", "");
-      chatBot.setPredicate("null", "");
-      // load last user session
-      if (!chatBot.getPredicate("name").isEmpty()) {
-        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown") || chatBot.getPredicate("lastUsername").equals("default")) {
-          chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
-        }
-      }
-      chatBot.setPredicate("parameterHowDoYouDo", "");
-      chatBot.savePredicates();
-      htmlFilter = (HtmlFilter) startPeer("htmlFilter");// Runtime.start("htmlFilter",
-      // "HtmlFilter");
-      chatBot.attachTextListener(htmlFilter);
-      htmlFilter.attachTextListener((TextListener) getPeer("mouth"));
-      chatBot.attachTextListener(this);
-      // start session based on last recognized person
-      // if (!chatBot.getPredicate("default", "lastUsername").isEmpty() &&
-      // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
-      // chatBot.startSession(chatBot.getPredicate("lastUsername"));
-      // }
-      if (chatBot.getPredicate("default", "firstinit").isEmpty() || chatBot.getPredicate("default", "firstinit").equals("unknown")
-          || chatBot.getPredicate("default", "firstinit").equals("started")) {
-        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
-        invoke("publishEvent", "FIRST INIT");
-      } else {
-        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
-        invoke("publishEvent", "WAKE UP");
-      }
-    } catch (Exception e) {
-      speak("could not load chatBot");
-      error(e.getMessage());
-      speak(e.getMessage());
-    }
-    broadcastState();
-    return chatBot;
-  }
-
-  @Deprecated /* use startPeer */
-  public SpeechRecognizer startEar() {
-
-    ear = (SpeechRecognizer) startPeer("ear");
-    ear.attachSpeechSynthesis((SpeechSynthesis) getPeer("mouth"));
-    ear.attachTextListener(chatBot);
-    broadcastState();
-    return ear;
-  }
-
   public void startedGesture() {
     startedGesture("unknown");
   }
diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java
index 6b0aad8f00..c32a0ab437 100644
--- a/src/main/java/org/myrobotlab/service/ProgramAB.java
+++ b/src/main/java/org/myrobotlab/service/ProgramAB.java
@@ -711,12 +711,25 @@ public Session startSession(String path, String userName, String botName, java.u
     }
 
     session = new Session(this, userName, botInfo);
-    sessions.put(getSessionKey(userName, botName), session);
+    String sessionKey = getSessionKey(userName, botName);
+    sessions.put(sessionKey, session);
 
     log.info("Started session for bot botName:{} , userName:{}", botName, userName);
     setCurrentSession(userName, botName);
+    
+    invoke("publishSession", sessionKey);
+    
     return session;
   }
+  
+  /**
+   * When a new session is started this event is published with the session's key
+   * @param sessionKey of new Session
+   * @return sessionKey
+   */
+  public String publishSession(String sessionKey) {
+    return sessionKey;
+  }
 
   /**
    * setting the current session is equivalent to setting current user name and
diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java
index d018df206a..fd3ce94187 100644
--- a/src/main/java/org/myrobotlab/service/Random.java
+++ b/src/main/java/org/myrobotlab/service/Random.java
@@ -229,11 +229,21 @@ public void run() {
           // and see if any random event needs processing
 
           sleep(config.rate);
-          for (String key : randomData.keySet()) {
+          // copy to avoid concurrent exceptions, avoid iterating over randomData
+          Map<String, RandomMessage> tasks = new HashMap<>(); 
+          Set<String> keySet = new HashSet<String>(randomData.keySet());
+          for (String k : keySet) {
+            RandomMessage rm = randomData.get(k);
+            if (rm != null) {
+              tasks.put(k, rm);
+            }
+          }
+          
+          for (String key : tasks.keySet()) {
 
             long now = System.currentTimeMillis();
 
-            RandomMessage randomEntry = randomData.get(key);
+            RandomMessage randomEntry = tasks.get(key);
             if (!randomEntry.enabled) {
               continue;
             }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 49575f58c4..d5e7116605 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -37,7 +37,7 @@ public class InMoov2Config extends ServiceConfig {
 
   public boolean flashOnErrors = true;
 
-  public boolean flashOnPir;
+  public boolean flashOnPir = false;
 
   public boolean forceMicroOnIfSleeping = true;
 
@@ -276,7 +276,8 @@ public Plan getDefault(Plan plan, String name) {
       }
     }
 
-    chatBot.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
+    chatBot.listeners.add(new Listener("publishText", getPeerName("htmlFilter"), "onText"));
+    chatBot.listeners.add(new Listener("publishSession", name));
 
     Gpt3Config gpt3 = (Gpt3Config) plan.get(getPeerName("gpt3"));
     gpt3.listeners.add(new Listener("publishText", name + ".htmlFilter", "onText"));
diff --git a/src/test/java/org/myrobotlab/service/HarryTest.java b/src/test/java/org/myrobotlab/service/HarryTest.java
index 29527a0845..1d1995fc5f 100755
--- a/src/test/java/org/myrobotlab/service/HarryTest.java
+++ b/src/test/java/org/myrobotlab/service/HarryTest.java
@@ -228,7 +228,7 @@ public void testHarry() throws Exception {
     // if startInMoov:
     // i01.startAll(leftPort, rightPort)
     // else:
-    i01.mouth = mouth;
+    i01.startPeer("mouth");
 
     solr.attachAllInboxes();
     solr.attachAllOutboxes();

From 7b0bfd070c4a183cf50adbadccad272bd72bc1aa Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 22 Feb 2024 07:39:37 -0800
Subject: [PATCH 087/131] updates

---
 .../org/myrobotlab/programab/Session.java     |   9 +-
 .../java/org/myrobotlab/service/InMoov2.java  |  52 +++--
 src/main/java/org/myrobotlab/service/Log.java |   5 +-
 .../java/org/myrobotlab/service/Random.java   |  44 ++--
 .../java/org/myrobotlab/service/Runtime.java  |  16 +-
 .../abstracts/AbstractSpeechSynthesis.java    | 192 +++++++-----------
 .../service/config/AudioFileConfig.java       |   8 +-
 .../service/config/InMoov2Config.java         |   6 +-
 .../service/config/SpeechSynthesisConfig.java |  12 +-
 9 files changed, 173 insertions(+), 171 deletions(-)

diff --git a/src/main/java/org/myrobotlab/programab/Session.java b/src/main/java/org/myrobotlab/programab/Session.java
index a234310e57..a76fb94bec 100644
--- a/src/main/java/org/myrobotlab/programab/Session.java
+++ b/src/main/java/org/myrobotlab/programab/Session.java
@@ -5,7 +5,9 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
 
@@ -122,7 +124,12 @@ public void savePredicates() {
    */
   public Map<String, String> getPredicates() {
     TreeMap<String, String> sort = new TreeMap<>();
-    sort.putAll(getChat().predicates);
+    // copy keys, making this sort thread safe
+    Set<String> keys = new HashSet(getChat().predicates.keySet());
+    for (String key: keys) {
+      String value = getChat().predicates.get(key);
+      sort.put(key, value);
+    }
     return sort;
   }
 
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 2322a4bc8e..c496da1a61 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -98,15 +98,10 @@ public void stop() {
   }
 
   public static class Heartbeat {
-    double batteryLevel = 100;
     public long count = 0;
-    public List<LogEntry> errors;
-    public String state;
     public long ts = System.currentTimeMillis();
 
     public Heartbeat(InMoov2 inmoov) {
-      this.state = inmoov.state;
-      this.errors = inmoov.errors;
       this.count = inmoov.heartbeatCount;
     }
   }
@@ -581,9 +576,9 @@ public void configToPredicates() {
           if (sessions != null) {
             for (Session session : sessions.values()) {
               if (value != null) {
-                session.setPredicate(field.getName(), value.toString());
+                session.setPredicate(String.format("config.%s", field.getName()), value.toString());
               } else {
-                session.setPredicate(field.getName(), null);
+                session.setPredicate(String.format("config.%s", field.getName()), null);
               }
 
             }
@@ -640,6 +635,23 @@ public void displayFullScreen(String src) {
       error("could not display picture %s", src);
     }
   }
+  
+  public void enableRandomHead() {
+    Random random = (Random)getPeer("random");
+    if (random != null) {
+      random.disableAll();
+      random.enable(String.format("%s.setHeadSpeed", getName()));
+      random.enable(String.format("%s.moveHead", getName()));
+      random.enable();
+    }
+  }
+  
+  public void disableRandom() {
+    Random random = (Random)getPeer("random");
+    if (random != null) {
+      random.disable();
+    }    
+  }
 
   public void enable() {
     sendToPeer("head", "enable");
@@ -1155,6 +1167,7 @@ public void onCreated(String fullname) {
 
   @Override
   public void onEndSpeaking(String utterance) {
+    processMessage("onEndSpeaking", utterance);
     isSpeaking = false;
   }
 
@@ -1337,9 +1350,9 @@ public void onStarted(String name) {
     }
   }
 
-  // FIXME - rebroadcast these
   @Override
   public void onStartSpeaking(String utterance) {
+    processMessage("onStartSpeaking", utterance);
     isSpeaking = true;
   }
 
@@ -1402,7 +1415,7 @@ public void processMessage(String method) {
    * @param method
    * @param data
    */
-  public void processMessage(String method, Object data) {
+  public void processMessage(String method, Object ... data) {
     // User processing should not occur until after boot has completed
     if (!state.equals("boot")) {
       // FIXME - this needs to be in config
@@ -1490,7 +1503,7 @@ public Heartbeat publishHeartbeat() {
       if ("boot".equals(state)) {
         // continue booting - we don't put heartbeats in user/python space
         // until java-land is done booting
-        log.info("boot hasn't completed, will not process heartbeat");
+        log.info("boot hasn't completed, will not process heartbeat - trying boot");
         boot();
         return heartbeat;
       }
@@ -1721,6 +1734,9 @@ public StateChange publishStateChange(StateChange stateChange) {
 
     lastState = state;
     state = stateChange.state;
+    
+    setPredicate(String.format("%s.end", lastState), System.currentTimeMillis());
+    setPredicate(String.format("%s.start", state), System.currentTimeMillis());
 
     processMessage("onStateChange", stateChange);
 
@@ -2185,15 +2201,15 @@ public void systemCheck() {
     // TODO change to experimental :)
     String version = ("unknownVersion".equals(platform.getVersion())) ? "experimental" : platform.getVersion();
 
-    setPredicate("system_version", version);
-    setPredicate("system_uptime", Runtime.getUptime());
-    setPredicate("system_servo_count", servoCount);
-    setPredicate("system_service_count", Runtime.getServices().size());
-    setPredicate("system_free_memory", Runtime.getFreeMemory() / 1000000);
-    setPredicate("system_errors_exist", errors.size() > 0);
-    setPredicate("system_error_count", errors.size());
-    setPredicate("system_battery_level", Runtime.getBatteryLevel());
+    setPredicate("system.version", version);
+    setPredicate("system.uptime", Runtime.getUptime());
+    setPredicate("system.servoCount", servoCount);
+    setPredicate("system.serviceCount", Runtime.getServices().size());
+    setPredicate("system.freeMemory", Runtime.getFreeMemory() / 1000000);
+    setPredicate("system.errorsExist", errors.size() > 0);
+    setPredicate("system.errorCount", errors.size());
     setPredicate("state", getState());
+    setPredicate("system.batteryLevel", Runtime.getBatteryLevel().intValue());
 
   }
 
diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java
index dc20806cf2..51e388a994 100644
--- a/src/main/java/org/myrobotlab/service/Log.java
+++ b/src/main/java/org/myrobotlab/service/Log.java
@@ -206,7 +206,10 @@ synchronized public void flush() {
       
       List<LogEntry> errors = new ArrayList<>();
       for(int i = 0; i < buffer.size(); ++i) {
-        errors.add(buffer.get(i));
+        LogEntry entry = buffer.get(i);
+        if ("ERROR".equals(entry.level)) {
+          errors.add(entry);
+        }
       }
       if (errors.size() > 0) {
         invoke("publishErrors", errors);
diff --git a/src/main/java/org/myrobotlab/service/Random.java b/src/main/java/org/myrobotlab/service/Random.java
index fd3ce94187..b419e7b8eb 100644
--- a/src/main/java/org/myrobotlab/service/Random.java
+++ b/src/main/java/org/myrobotlab/service/Random.java
@@ -30,7 +30,7 @@ public class Random extends Service<RandomConfig> {
 
   private static final long serialVersionUID = 1L;
 
-  public final static Logger log = LoggerFactory.getLogger(Random.class);
+  protected final static Logger log = LoggerFactory.getLogger(Random.class);
 
   transient private RandomProcessor processor = null;
 
@@ -75,7 +75,7 @@ public Range(Object min, Object max) {
   /**
    * all random message data is located here
    */
-  Map<String, RandomMessage> randomData = new HashMap<>();
+  protected Map<String, RandomMessage> randomData = new HashMap<>();
 
   /**
    * Java's random value generator
@@ -107,7 +107,7 @@ public long getRandom(long min, long max) {
   public double getRandom(double min, double max) {
     return min + (Math.random() * (max - min));
   }
-  
+
   public RandomMessage getTask(String taskName) {
     return randomData.get(taskName);
   }
@@ -210,7 +210,9 @@ public void addRandom(String taskName, long minIntervalMs, long maxIntervalMs, S
     data.data = ranges;
     data.enabled = true;
 
-    randomData.put(taskName, data);
+    synchronized (lock) {
+      randomData.put(taskName, data);
+    }
 
     log.info("add random message {} in {} to {} ms", taskName, data.minIntervalMs, data.maxIntervalMs);
     broadcastState();
@@ -229,16 +231,22 @@ public void run() {
           // and see if any random event needs processing
 
           sleep(config.rate);
-          // copy to avoid concurrent exceptions, avoid iterating over randomData
-          Map<String, RandomMessage> tasks = new HashMap<>(); 
-          Set<String> keySet = new HashSet<String>(randomData.keySet());
-          for (String k : keySet) {
-            RandomMessage rm = randomData.get(k);
-            if (rm != null) {
-              tasks.put(k, rm);
+          
+          Map<String, RandomMessage> tasks = null;
+          synchronized (lock) {
+
+            // copy to avoid concurrent exceptions, avoid iterating over
+            // randomData
+            tasks = new HashMap<>();
+            Set<String> keySet = new HashSet<String>(randomData.keySet());
+            for (String k : keySet) {
+              RandomMessage rm = randomData.get(k);
+              if (rm != null) {
+                tasks.put(k, rm);
+              }
             }
           }
-          
+
           for (String key : tasks.keySet()) {
 
             long now = System.currentTimeMillis();
@@ -313,7 +321,7 @@ public RandomConfig getConfig() {
     super.getConfig();
 
     config.enabled = enabled;
-    
+
     if (config.randomMessages == null) {
       config.randomMessages = new HashMap<>();
     }
@@ -445,15 +453,15 @@ public List<MethodEntry> methodQuery(String serviceName, String methodName) {
     }
     return MethodCache.getInstance().query(si.getClass().getCanonicalName(), methodName);
   }
-  
-  public Map<String, RandomMessage> getRandomEvents(){
+
+  public Map<String, RandomMessage> getRandomEvents() {
     return randomData;
   }
-  
+
   public RandomMessage getRandomEvent(String key) {
     return randomData.get(key);
   }
-  
+
   /**
    * disables all the individual tasks
    */
@@ -463,7 +471,7 @@ public void disableAll() {
     }
     broadcastState();
   }
-  
+
   @Override
   public void releaseService() {
     disable();
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index d8a020dd84..e203a65b81 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -910,12 +910,14 @@ public static Runtime getInstance() {
             runtime.apply(c);
           }
 
-          if (options.services != null) {
+          if (options.services != null && options.services.size() != 0) {
             log.info("command line override for services created");
             createAndStartServices(options.services);
           } else {
             log.info("processing config.registry");
-            if (startYml.enable) {
+            if (options.config != null) {
+              Runtime.startConfig(options.config);
+            } else if (startYml.enable) {
               Runtime.startConfig(startYml.config);
             }
           }
@@ -1544,7 +1546,7 @@ static public void install(String serviceType, Boolean blocking) {
       if (blocking == null) {
         blocking = false;
       }
-      
+
       if (installerThread != null) {
         log.error("another request to install dependencies, 1st request has not completed");
         return;
@@ -1571,7 +1573,7 @@ public void run() {
       } else {
         installerThread.start();
       }
-      
+
       installerThread = null;
     }
   }
@@ -4913,16 +4915,16 @@ static public void releaseConfigPath(String configPath) {
       RuntimeConfig config = CodecUtils.fromYaml(releaseData, RuntimeConfig.class);
       List<String> registry = config.getRegistry();
       Collections.reverse(Arrays.asList(registry));
-      
+
       // get starting services if any entered on the command line
-      // -s log Log webgui WebGui ... etc - these will be protected 
+      // -s log Log webgui WebGui ... etc - these will be protected
       List<String> startingServices = new ArrayList<>();
       if (options.services.size() % 2 == 0) {
         for (int i = 0; i < options.services.size(); i += 2) {
           startingServices.add(options.services.get(i));
         }
       }
-      
+
       for (String name : registry) {
         if (startingServices.contains(name)) {
           continue;
diff --git a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java
index f8cca2fa92..011f1f6e08 100644
--- a/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java
+++ b/src/main/java/org/myrobotlab/service/abstracts/AbstractSpeechSynthesis.java
@@ -6,9 +6,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
 
 import org.myrobotlab.framework.Service;
 import org.myrobotlab.framework.interfaces.Attachable;
@@ -42,22 +40,11 @@ public abstract class AbstractSpeechSynthesis<C extends SpeechSynthesisConfig> e
 
   public static final String journalFilename = "journal.txt";
 
-  /**
-   * substitutions are phonetic substitutions for a specific instance of speech
-   * synthesis service
-   */
-  transient protected Map<String, String> substitutions = new ConcurrentHashMap<String, String>();
-
   /**
    * generalized list of languages and their codes - if useful
    */
   protected Map<String, Locale> locales = new HashMap<>();
 
-  /**
-   * mute or unmute service
-   */
-  boolean mute = false;
-
   /**
    * replaces key with replacement
    */
@@ -252,8 +239,6 @@ public Object getVoiceProvider() {
 
   private List<Voice> voiceList = new ArrayList<>();
 
-  protected boolean blocking = false;
-
   // FIXME - deprecate - begin using SSML
   // specific effects and effect notation needs to be isolated to the
   // implementing service
@@ -291,7 +276,7 @@ public AbstractSpeechSynthesis(String n, String id) {
     // should hold off creating or starting peers until the service has started
     // audioFile = (AudioFile) createPeer("audioFile");
 
-//     getVoices();
+    // getVoices();
 
   }
 
@@ -571,65 +556,65 @@ public String publishSpeechRequested(String toSpeak) {
    * @return - list of audio data
    */
   public List<AudioData> parse(String toSpeak) {
-    
+
     // we generate a list of audio data to play to support
     // synthesizing this speech
     List<AudioData> playList = new ArrayList<AudioData>();
-    
+
     try {
 
-    // TODO - not sure if we want to support this notation
-    // but at the moment it seems useful
-    // splitting on sound effects ...
-    // TODO - use SSML speech synthesis markup language
+      // TODO - not sure if we want to support this notation
+      // but at the moment it seems useful
+      // splitting on sound effects ...
+      // TODO - use SSML speech synthesis markup language
 
-    log.info("{} processing {}", getName(), toSpeak);
+      log.info("{} processing {}", getName(), toSpeak);
 
-    // broadcast the original text to be processed/parsed
-    invoke("publishSpeechRequested", toSpeak);
+      // broadcast the original text to be processed/parsed
+      invoke("publishSpeechRequested", toSpeak);
 
-    // normalize to lower case
-    toSpeak = toSpeak.toLowerCase();
+      // normalize to lower case
+      toSpeak = toSpeak.toLowerCase();
 
-    // process substitutions
-    if (substitutions != null) {
-      for (String substitute : substitutions.keySet()) {
-        toSpeak = toSpeak.replace(substitute, substitutions.get(substitute));
+      // process substitutions
+      if (config.substitutions != null) {
+        for (String substitute : config.substitutions.keySet()) {
+          toSpeak = toSpeak.replace(substitute, config.substitutions.get(substitute));
+        }
       }
-    }
 
-    List<String> spokenParts = parseEffects(toSpeak);
+      List<String> spokenParts = parseEffects(toSpeak);
 
-    toSpeak = filterText(toSpeak);
+      toSpeak = filterText(toSpeak);
 
-    for (String speak : spokenParts) {
+      for (String speak : spokenParts) {
 
-      AudioData audioData = null;
-      if (speak.startsWith("#") && speak.endsWith("#")) {
-        audioData = new AudioData(
-            System.getProperty("user.dir") + File.separator + "audioFile" + File.separator + "voiceEffects" + File.separator + speak.substring(1, speak.length() - 1) + ".mp3");
-      } else {
-        audioData = new AudioData(getLocalFileName(speak));
-      }
+        AudioData audioData = null;
+        if (speak.startsWith("#") && speak.endsWith("#")) {
+          audioData = new AudioData(
+              System.getProperty("user.dir") + File.separator + "audioFile" + File.separator + "voiceEffects" + File.separator + speak.substring(1, speak.length() - 1) + ".mp3");
+        } else {
+          audioData = new AudioData(getLocalFileName(speak));
+        }
 
-      if (speak.trim().length() == 0) {
-        continue;
-      }
+        if (speak.trim().length() == 0) {
+          continue;
+        }
 
-      if (!mute) {
-        process(audioData, speak, blocking);
-      } else {
-        log.info("not producing audio for {} - currently we are mute", speak);
-      }
+        if (!config.mute) {
+          process(audioData, speak, config.blocking);
+        } else {
+          log.info("not producing audio for {} - currently we are mute", speak);
+        }
 
-      // effect files are handled differently from generated audio
-      playList.add(audioData);
-    }
-    // FIXME - in theory "speaking" means generating audio from some text
-    // so starting speaking event is when the first audio is "started"
-    // and finished speaking is when the last audio is finished
+        // effect files are handled differently from generated audio
+        playList.add(audioData);
+      }
+      // FIXME - in theory "speaking" means generating audio from some text
+      // so starting speaking event is when the first audio is "started"
+      // and finished speaking is when the last audio is finished
 
-    } catch(Exception e) {
+    } catch (Exception e) {
       error(e);
     }
     return playList;
@@ -647,12 +632,12 @@ public void addSubstitution(String key, String replacement) {
    */
   @Override
   public void replaceWord(String key, String replacement) {
-    substitutions.put(key.toLowerCase(), replacement.toLowerCase());
+    config.substitutions.put(key.toLowerCase(), replacement.toLowerCase());
   }
 
   @Override
   public void replaceWord(WordFilter filter) {
-    substitutions.put(filter.word.toLowerCase(), filter.substitute.toLowerCase());
+    config.substitutions.put(filter.word.toLowerCase(), filter.substitute.toLowerCase());
   }
 
   public Long publishGenerationTime(Long timeMs) {
@@ -706,10 +691,10 @@ public List<AudioData> speak(String toSpeak) {
 
   @Override
   public List<AudioData> speakBlocking(String toSpeak) {
-    boolean prevValue = blocking;
-    blocking = true;
+    boolean prevValue = config.blocking;
+    config.blocking = true;
     List<AudioData> audioData = parse(toSpeak);
-    blocking = prevValue;
+    config.blocking = prevValue;
     return audioData;
   }
 
@@ -954,35 +939,34 @@ public boolean setLanguage(String lang) {
     }
     return false;
   }
-  
 
   @Override
   public boolean setVoice(String name) {
-      if (voices == null) {
-          return false;
-      }
+    if (voices == null) {
+      return false;
+    }
 
-      SpeechSynthesisConfig config = (SpeechSynthesisConfig)this.config;
-      voice = voices.get(name);
-      
-      if (voice == null) {
-        voice = voiceKeyIndex.get(name);
-      }
-      
-      if (voice == null) {
-        voice = voiceProviderIndex.get(name);
-      }
-      
-      if (voice == null) {
-          error("could not set voice %s - valid voices are %s", name, String.join(", ", getVoiceNames()));
-          return false;
-      }
+    SpeechSynthesisConfig config = (SpeechSynthesisConfig) this.config;
+    voice = voices.get(name);
+
+    if (voice == null) {
+      voice = voiceKeyIndex.get(name);
+    }
+
+    if (voice == null) {
+      voice = voiceProviderIndex.get(name);
+    }
 
-      config.voice = name;
-      broadcastState();
-      return true;
+    if (voice == null) {
+      error("could not set voice %s - valid voices are %s", name, String.join(", ", getVoiceNames()));
+      return false;
+    }
+
+    config.voice = name;
+    broadcastState();
+    return true;
   }
-  
+
   public boolean setVoice(Integer index) {
     if (index > voiceList.size() || index < 0) {
       error("setVoice({}) not valid pick range 0 to {}", index, voiceList.size());
@@ -1102,49 +1086,30 @@ public void unmute() {
 
   @Override
   public void setMute(boolean b) {
-    this.mute = b;
+    this.config.mute = b;
   }
 
   @Override
   public Boolean setBlocking(Boolean b) {
-    blocking = b;
+    config.blocking = b;
     return b;
   }
 
   public boolean isMute() {
-    return mute;
+    return config.mute;
   }
 
   @Override
   public C apply(C c) {
     super.apply(c);
-
-    setMute(c.mute);
-
-    setBlocking(c.blocking);
-
-    if (c.substitutions != null) {
-      for (String n : c.substitutions.keySet()) {
-        replaceWord(n, c.substitutions.get(n));
-      }
-    }
+    
     // some systems require querying set of voices
     getVoices();
-    
+
     if (c.voice != null) {
       setVoice(c.voice);
     }
 
-    if (c.speechRecognizers != null) {
-      for (String name : c.speechRecognizers) {
-        try {
-          attachSpeechListener(name);
-        } catch (Exception e) {
-          error(e);
-        }
-      }
-    }
-
     return c;
   }
 
@@ -1160,18 +1125,9 @@ public void attachSpeechControl(SpeechSynthesisControl control) {
   @Override
   public C getConfig() {
     C c = super.getConfig();
-    c.mute = mute;
-    c.blocking = blocking;
-    if (substitutions != null && !substitutions.isEmpty()) {
-      c.substitutions = new HashMap<>();
-      c.substitutions.putAll(substitutions);
-    }
     if (voice != null) {
       c.voice = voice.name;
     }
-    Set<String> listeners = getAttached("publishStartSpeaking");
-    c.speechRecognizers = listeners.toArray(new String[0]);
-
     return c;
   }
 
diff --git a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
index 47b3dc9b91..1a91e3c096 100644
--- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
@@ -12,11 +12,11 @@ public class AudioFileConfig extends ServiceConfig {
   
   public String currentPlaylist = "default";
   
+  /**
+   * Named map of lists of files
+   */
   public Map<String, List<String>> playlists = new TreeMap<>();
-  
-  @Deprecated /* use regular "listeners" from ServiceConfig parent */
-  public String[] audioListeners;
-  
+    
   /**
    * a multiplier to scale amplitude of output waveform
    */
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index d5e7116605..d9cd38d947 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -374,12 +374,12 @@ public Plan getDefault(Plan plan, String name) {
     fsm.transitions.add(new Transition("boot", "wake", "wake"));
     // setup, nor sleep should be affected by idle
     fsm.transitions.add(new Transition("setup", "setup_done", "idle"));
-    fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("random", "idle", "idle"));
     fsm.transitions.add(new Transition("idle", "sleep", "sleep"));
+    fsm.transitions.add(new Transition("idle", "power_down", "power_down"));
+    fsm.transitions.add(new Transition("idle", "random", "random"));
     fsm.transitions.add(new Transition("sleep", "wake", "wake"));
     fsm.transitions.add(new Transition("sleep", "power_down", "power_down"));
-    fsm.transitions.add(new Transition("idle", "power_down", "power_down"));
     fsm.transitions.add(new Transition("wake", "setup", "setup"));
     fsm.transitions.add(new Transition("wake", "idle", "idle"));
     fsm.transitions.add(new Transition("idle", "setup", "setup"));
@@ -565,7 +565,9 @@ public Plan getDefault(Plan plan, String name) {
     fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
     
     // peer --to--> peer
+    mouth.listeners.add(new Listener("publishStartSpeaking", name));
     mouth.listeners.add(new Listener("publishStartSpeaking", getPeerName("ear")));
+    mouth.listeners.add(new Listener("publishEndSpeaking", name));
     mouth.listeners.add(new Listener("publishEndSpeaking", getPeerName("ear")));
 
     return plan;
diff --git a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
index c84c4dca65..fc63167ffb 100644
--- a/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/SpeechSynthesisConfig.java
@@ -6,11 +6,19 @@
 
 public class SpeechSynthesisConfig extends ServiceConfig {
 
+  /**
+   * mute or unmute service
+   */
   public boolean mute = false;
+
   public boolean blocking = false;
-  @Deprecated /* :(  ... this is already in listeners ! */
-  public String[] speechRecognizers;
+
+  /**
+   * substitutions are phonetic substitutions for a specific instance of speech
+   * synthesis service
+   */
   public Map<String, String> substitutions;
+
   public String voice;
 
   @Override

From 814de152115a67476a1e257d21af5a7892f43462 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 22 Feb 2024 07:58:59 -0800
Subject: [PATCH 088/131] fixed type

---
 src/main/java/org/myrobotlab/image/WebImage.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/image/WebImage.java b/src/main/java/org/myrobotlab/image/WebImage.java
index fd4ad4e02f..4b5b41842d 100644
--- a/src/main/java/org/myrobotlab/image/WebImage.java
+++ b/src/main/java/org/myrobotlab/image/WebImage.java
@@ -56,7 +56,7 @@ public WebImage(final BufferedImage img, final String source, Integer frameIndex
       if (quality == null) {
         ImageIO.write(img, imgType, os);
         os.close();
-        data = String.format("data:image/%s;base64,%s", type,CodecUtils.toBase64(os.toByteArray()));
+        data = String.format("data:image/%s;base64,%s", imgType,CodecUtils.toBase64(os.toByteArray()));
       } else {
 
         // save jpeg image with specific quality. "1f" corresponds to 100% ,

From db1f7d17dd48282fcfccf4626c95a6fe44544d63 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 22 Feb 2024 07:59:08 -0800
Subject: [PATCH 089/131] webgui

---
 src/main/java/org/myrobotlab/service/WebGui.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index f50cdc238a..4367ae8c2a 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -1177,7 +1177,8 @@ public static void main(String[] args) {
 
     try {
 
-      Runtime.main(new String[] { "--log-level", "info", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
+      Runtime.main(new String[] { "-c", "worky"});
       // Runtime.main(new String[] { "--install" });
 
       boolean done = true;

From 7470e0371698f25bac93c539aa6461d55381f755 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 22 Feb 2024 09:14:17 -0800
Subject: [PATCH 090/131] getWebImage

---
 .../java/org/myrobotlab/service/OpenCV.java   | 131 ++++++++++--------
 1 file changed, 71 insertions(+), 60 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java
index dcd7ad08d9..474a8055d2 100644
--- a/src/main/java/org/myrobotlab/service/OpenCV.java
+++ b/src/main/java/org/myrobotlab/service/OpenCV.java
@@ -155,7 +155,7 @@ class VideoProcessor implements Runnable {
 
     @Override
     synchronized public void run() {
-      // create a closeable frame converter     
+      // create a closeable frame converter
       CloseableFrameConverter converter = new CloseableFrameConverter();
 
       try {
@@ -308,16 +308,11 @@ public void start() {
   transient final static public String PART = "part";
   static final String TEST_LOCAL_FACE_FILE_JPEG = "src/test/resources/OpenCV/multipleFaces.jpg";
 
-  public final static String POSSIBLE_FILTERS[] = { "AdaptiveThreshold", "AddMask", "Affine", "And", "BlurDetector",
-      "BoundingBoxToFile", "Canny", "ColorTrack", "Copy",
-      "CreateHistogram", "Detector", "Dilate", "DL4J", "DL4JTransfer", "Erode", "FaceDetect", "FaceDetectDNN",
-      "FaceRecognizer", "FaceTraining", "Fauvist", "FindContours", "Flip",
-      "FloodFill", "FloorFinder", "FloorFinder2", "GoodFeaturesToTrack", "Gray", "HoughLines2", "Hsv", "ImageSegmenter",
-      "Input", "InRange", "Invert", "KinectDepth",
-      "KinectDepthMask", "KinectNavigate", "LKOpticalTrack", "Lloyd", "Mask", "MatchTemplate", "MiniXception",
-      "MotionDetect", "Mouse", "Output", "Overlay", "PyramidDown",
-      "PyramidUp", "ResetImageRoi", "Resize", "SampleArray", "SampleImage", "SetImageROI", "SimpleBlobDetector",
-      "Smooth", "Solr", "Split", "SURF", "Tesseract", "TextDetector",
+  public final static String POSSIBLE_FILTERS[] = { "AdaptiveThreshold", "AddMask", "Affine", "And", "BlurDetector", "BoundingBoxToFile", "Canny", "ColorTrack", "Copy",
+      "CreateHistogram", "Detector", "Dilate", "DL4J", "DL4JTransfer", "Erode", "FaceDetect", "FaceDetectDNN", "FaceRecognizer", "FaceTraining", "Fauvist", "FindContours", "Flip",
+      "FloodFill", "FloorFinder", "FloorFinder2", "GoodFeaturesToTrack", "Gray", "HoughLines2", "Hsv", "ImageSegmenter", "Input", "InRange", "Invert", "KinectDepth",
+      "KinectDepthMask", "KinectNavigate", "LKOpticalTrack", "Lloyd", "Mask", "MatchTemplate", "MiniXception", "MotionDetect", "Mouse", "Output", "Overlay", "PyramidDown",
+      "PyramidUp", "ResetImageRoi", "Resize", "SampleArray", "SampleImage", "SetImageROI", "SimpleBlobDetector", "Smooth", "Solr", "Split", "SURF", "Tesseract", "TextDetector",
       "Threshold", "Tracker", "Transpose", "Undistort", "Yolo" };
 
   static final long serialVersionUID = 1L;
@@ -649,7 +644,7 @@ synchronized public OpenCVFilter addFilter(OpenCVFilter filter) {
    * add filter by type e.g. addFilter("Canny","Canny")
    * 
    * @param filterName
-   *                   - name of filter
+   *          - name of filter
    * @return the filter
    */
   public CVFilter addFilter(String filterName) {
@@ -689,7 +684,7 @@ public void capture(FrameGrabber grabber) throws org.bytedeco.javacv.FrameGrabbe
    * capture from a camera
    * 
    * @param cameraIndex
-   *                    the camera index to capture from
+   *          the camera index to capture from
    */
   public void capture(Integer cameraIndex) {
     if (cameraIndex == null) {
@@ -710,7 +705,7 @@ public void capture(Integer cameraIndex) {
    * its the most capable of decoding different filetypes.
    * 
    * @param filename
-   *                 the file to use as the input filename.
+   *          the file to use as the input filename.
    * 
    */
   public void capture(String filename) {
@@ -723,7 +718,7 @@ public void capture(String filename) {
   public void captureFromResourceFile(String filename) throws IOException {
     capture(filename);
   }
-  
+
   /**
    * Gets valid camera indexes by iterating through 8
    * 
@@ -761,7 +756,7 @@ public List<Integer> getCameraIndexes() {
 
     return cameraIndexes;
   }
-  
+
   public int getCameraIndex() {
     return this.cameraIndex;
   }
@@ -851,7 +846,7 @@ public List<Classification> getFaces(int timeout) {
    * get a filter by name
    * 
    * @param name
-   *             filter name to lookup
+   *          filter name to lookup
    * @return the filter by name o/w null
    * 
    */
@@ -881,8 +876,7 @@ public OpenCVData getGoodFeatures() {
   }
 
   public FrameGrabber getGrabber()
-      throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException,
-      InvocationTargetException, org.bytedeco.javacv.FrameGrabber.Exception {
+      throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, org.bytedeco.javacv.FrameGrabber.Exception {
 
     if (grabber != null) {
       return grabber;
@@ -907,8 +901,7 @@ public FrameGrabber getGrabber()
       // get and cache image file
       // FIXME - perhaps "test" stream to try to determine what "type" it is -
       // mjpeg/jpg/gif/ octet-stream :( ???
-      if (grabberType == null
-          || (grabberType != null && (!grabberType.equals("MJpeg") && !grabberType.equals("IPCamera")))) {
+      if (grabberType == null || (grabberType != null && (!grabberType.equals("MJpeg") && !grabberType.equals("IPCamera")))) {
         inputFile = getImageFromUrl(inputFile);
       }
     }
@@ -920,8 +913,7 @@ public FrameGrabber getGrabber()
         ext = inputFile.substring(pos + 1).toLowerCase();
       }
     }
-    if (grabberType != null && (grabberType.equals("FFmpeg") || grabberType.equals("ImageFile"))
-        && inputSource.equals(INPUT_SOURCE_CAMERA)) {
+    if (grabberType != null && (grabberType.equals("FFmpeg") || grabberType.equals("ImageFile")) && inputSource.equals(INPUT_SOURCE_CAMERA)) {
       log.info("invalid state of ffmpeg and input source camera - setting to OpenCV frame grabber");
       grabberType = "OpenCV";
     }
@@ -976,8 +968,7 @@ public FrameGrabber getGrabber()
     }
 
     String prefixPath;
-    if (/* "IPCamera".equals(grabberType) || */ "Pipeline".equals(grabberType) || "ImageFile".equals(grabberType)
-        || "Sarxos".equals(grabberType) || "MJpeg".equals(grabberType)) {
+    if (/* "IPCamera".equals(grabberType) || */ "Pipeline".equals(grabberType) || "ImageFile".equals(grabberType) || "Sarxos".equals(grabberType) || "MJpeg".equals(grabberType)) {
       prefixPath = "org.myrobotlab.opencv.";
     } else {
       prefixPath = "org.bytedeco.javacv.";
@@ -1076,6 +1067,27 @@ public IplImage getImage() {
     return lastImage;
   }
 
+  /**
+   * "Easy" Base64 web image from display last frame
+   * 
+   * @return
+   */
+  public String getWebImage() {
+    try {
+      final ByteArrayOutputStream os = new ByteArrayOutputStream();
+      String imgType = "jpg";
+      BufferedImage bi = getDisplay();
+      if (bi != null) {
+        ImageIO.write(bi, imgType, os);
+        os.close();
+        return String.format("data:image/%s;base64,%s", imgType, CodecUtils.toBase64(os.toByteArray()));
+      }
+    } catch (Exception e) {
+      error(e);
+    }
+    return null;
+  }
+
   public String getInputFile() {
     return inputFile;
   }
@@ -1113,11 +1125,11 @@ public OpenCVData getOpenCVData(Integer timeout) {
    * appropriate filter through this method.
    * 
    * @param filterName
-   *                   the name of the fitler
+   *          the name of the fitler
    * @param method
-   *                   the method to invoke
+   *          the method to invoke
    * @param params
-   *                   the params to pass
+   *          the params to pass
    */
   public void invokeFilterMethod(String filterName, String method, Object... params) {
     OpenCVFilter filter = getFilter(filterName);
@@ -1136,7 +1148,10 @@ public boolean isRecording() {
     return recording;
   }
 
-  @Deprecated /* was used in SwingGui - nice feature through .. ability to undock displays */
+  @Deprecated /*
+               * was used in SwingGui - nice feature through .. ability to
+               * undock displays
+               */
   public boolean isUndocked() {
     return undockDisplay;
   }
@@ -1153,7 +1168,7 @@ synchronized public void pauseCapture() {
    * conversion from buffered image to base64 encoded jpg
    * 
    * @param img
-   *            the image to convert
+   *          the image to convert
    * @return base64jpeg version of buffered image
    */
   public String toBase64Jpg(BufferedImage img) {
@@ -1304,7 +1319,7 @@ private void processFilterStateUpdates(OpenCVFilter filter) {
    * base 64 jpg frame image
    * 
    * @param data
-   *             webimage data
+   *          webimage data
    * @return the web image data
    */
   public WebImage publishWebDisplay(WebImage data) {
@@ -1387,7 +1402,7 @@ public final SerializableImage publishDisplay(SerializableImage img) {
    * Publishing method for filters - used internally
    * 
    * @param filterWrapper
-   *                      wraps a filter
+   *          wraps a filter
    * 
    * @return FilterWrapper solves the problem of multiple types being resolved
    *         in the setFilterState(FilterWrapper data) method
@@ -1400,7 +1415,7 @@ public FilterWrapper publishFilterState(FilterWrapper filterWrapper) {
    * Publishing method for filters - uses string parameter for remote invocation
    * 
    * @param name
-   *             name of filter to publish state for
+   *          name of filter to publish state for
    * 
    * @return FilterWrapper solves the problem of multiple types being resolved
    *         in the setFilterState(FilterWrapper data) method
@@ -1436,7 +1451,7 @@ public void publishNoRecognizedFace() {
    * until asked for - then its cached SMART ! :)
    * 
    * @param data
-   *             the opencv data
+   *          the opencv data
    * @return cvdata
    * 
    */
@@ -1469,13 +1484,13 @@ public void putText(int x, int y, String format) {
    * creates a new overlay of text
    * 
    * @param x
-   *               coordinate
+   *          coordinate
    * @param y
-   *               coordinate
+   *          coordinate
    * @param format
-   *               format string
+   *          format string
    * @param color
-   *               color
+   *          color
    * 
    */
   public void putText(int x, int y, String format, String color) {
@@ -1487,9 +1502,9 @@ public void putText(int x, int y, String format, String color) {
    * the "light weight" put - it does not create any new cv objects
    * 
    * @param format
-   *               format for the text
+   *          format for the text
    * @param args
-   *               args to format into the text
+   *          args to format into the text
    * 
    */
   public void putText(String format, Object... args) {
@@ -1550,7 +1565,7 @@ public void startStreamer() {
    * key- input, filter, or display
    * 
    * @param data
-   *             data
+   *          data
    */
   public void record(OpenCVData data) {
     try {
@@ -1572,8 +1587,7 @@ public void record(OpenCVData data) {
          */
         FrameRecorder recorder = null;
         if (!recordingFrames) {
-          recordingFilename = String.format(getDataDir() + File.separator + "%s-%d.flv", recordingSource,
-              System.currentTimeMillis());
+          recordingFilename = String.format(getDataDir() + File.separator + "%s-%d.flv", recordingSource, System.currentTimeMillis());
           info("recording %s", recordingFilename);
           recorder = new FFmpegFrameRecorder(recordingFilename, frame.imageWidth, frame.imageHeight, 0);
           recorder.setFormat("flv");
@@ -1639,7 +1653,7 @@ public ImageData saveImage() {
 
   /**
    * @param name
-   *             remove a filter by name
+   *          remove a filter by name
    */
   @Override
   synchronized public void removeFilter(String name) {
@@ -1721,7 +1735,7 @@ public void setColor(String colorStr) {
    * enable() and setDisplayFilter() needed filter
    * 
    * @param name
-   *             name of the filter to set active
+   *          name of the filter to set active
    *
    */
   public void setActiveFilter(String name) {
@@ -1760,15 +1774,12 @@ public void setDisplayFilter(String name) {
 
   /**
    * @param otherFilter
-   *                    - data from remote source
+   *          - data from remote source
    * 
-   *                    This updates the filter with all the non-transient data in
-   *                    a
-   *                    remote copy through a reflective field update. If your
-   *                    filter has
-   *                    JNI members or pointer references it will break, mark all
-   *                    of
-   *                    these.
+   *          This updates the filter with all the non-transient data in a
+   *          remote copy through a reflective field update. If your filter has
+   *          JNI members or pointer references it will break, mark all of
+   *          these.
    */
   public void setFilterState(FilterWrapper otherFilter) {
     OpenCVFilter filter = getFilter(otherFilter.name);
@@ -1786,9 +1797,9 @@ public void setFilterState(FilterWrapper otherFilter) {
    * filter
    * 
    * @param name
-   *             name of the filter
+   *          name of the filter
    * @param data
-   *             state date to set.
+   *          state date to set.
    */
   public void setFilterState(String name, String data) {
     OpenCVFilter filter = getFilter(name);
@@ -1908,8 +1919,7 @@ public void recordFrames() {
   private boolean isSingleFrame() {
     if (inputSource.equals(INPUT_SOURCE_FILE) && inputFile != null) {
       String testExt = inputFile.toLowerCase();
-      if (testExt.endsWith(".jpg") || testExt.endsWith(".jpeg") || testExt.endsWith(".png") || testExt.endsWith(".gif")
-          || testExt.endsWith(".tiff") || testExt.endsWith(".tif")) {
+      if (testExt.endsWith(".jpg") || testExt.endsWith(".jpeg") || testExt.endsWith(".png") || testExt.endsWith(".gif") || testExt.endsWith(".tiff") || testExt.endsWith(".tif")) {
         return true;
       }
     }
@@ -1970,11 +1980,12 @@ public void enableFilter(String name) {
 
   /**
    * flip the video display vertically
+   * 
    * @param toFlip
    */
   public void flip(boolean toFlip) {
     config.flip = toFlip;
-    if (config.flip) {      
+    if (config.flip) {
       addFilter("Flip");
     } else {
       removeFilter("Flip");
@@ -2076,7 +2087,7 @@ public OpenCVConfig apply(OpenCVConfig c) {
         // TODO: better configuration of the filter when it's added.
       }
     }
-    
+
     flip(c.flip);
 
     if (c.capturing) {
@@ -2101,7 +2112,7 @@ public static void main(String[] args) throws Exception {
       // Runtime.start("python", "Python");
       OpenCV cv = (OpenCV) Runtime.start("cv", "OpenCV");
       cv.capture();
-      
+
       cv.addFilter(new OpenCVFilterYolo("yolo"));
       sleep(1000);
       cv.removeFilters();

From f155d0ae357a645dffe74a7beb72c341249f853f Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Thu, 22 Feb 2024 10:52:48 -0800
Subject: [PATCH 091/131] small config update

---
 src/main/java/org/myrobotlab/service/WebGui.java              | 4 ++--
 .../java/org/myrobotlab/service/config/InMoov2Config.java     | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 4367ae8c2a..e6efa1f28d 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -1177,8 +1177,8 @@ public static void main(String[] args) {
 
     try {
 
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
-      Runtime.main(new String[] { "-c", "worky"});
+      Runtime.main(new String[] { "--log-level", "warn", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
+      // Runtime.main(new String[] { "-c", "worky"});
       // Runtime.main(new String[] { "--install" });
 
       boolean done = true;
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index d9cd38d947..1ae722a633 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -516,7 +516,7 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishConfigFinished", name));
 
     LogConfig log = (LogConfig) plan.get(getPeerName("log"));
-    log.level = "WARN";
+    log.level = "warn";
     log.listeners.add(new Listener("publishErrors", name));
     // service --to--> InMoov2
 

From 1cdc49376865e19275609c538905d7107ee5e088 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 23 Feb 2024 21:55:07 -0800
Subject: [PATCH 092/131] remotespeech

---
 .../org/myrobotlab/service/LocalSpeech.java   | 85 +++++++++----------
 .../org/myrobotlab/service/RemoteSpeech.java  | 77 +++++++++++++++++
 .../service/config/RemoteSpeechConfig.java    | 21 +++++
 .../service/meta/RemoteSpeechMeta.java        | 38 +++++++++
 .../WebGui/app/service/js/RemoteSpeechGui.js  | 40 +++++++++
 .../WebGui/app/service/views/IntroGui.html    |  1 -
 .../app/service/views/RemoteSpeechGui.html    | 31 +++++++
 7 files changed, 247 insertions(+), 46 deletions(-)
 create mode 100644 src/main/java/org/myrobotlab/service/RemoteSpeech.java
 create mode 100644 src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
 create mode 100644 src/main/java/org/myrobotlab/service/meta/RemoteSpeechMeta.java
 create mode 100644 src/main/resources/resource/WebGui/app/service/js/RemoteSpeechGui.js
 create mode 100644 src/main/resources/resource/WebGui/app/service/views/RemoteSpeechGui.html

diff --git a/src/main/java/org/myrobotlab/service/LocalSpeech.java b/src/main/java/org/myrobotlab/service/LocalSpeech.java
index 3d2194c896..cb89016c82 100644
--- a/src/main/java/org/myrobotlab/service/LocalSpeech.java
+++ b/src/main/java/org/myrobotlab/service/LocalSpeech.java
@@ -77,8 +77,6 @@ public LocalSpeech(String n, String id) {
   @Override
   public AudioData generateAudioData(AudioData audioData, String toSpeak) throws IOException, InterruptedException {
 
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-
     // the actual filename on the file system
     String localFileName = getLocalFileName(toSpeak);
 
@@ -96,13 +94,13 @@ public AudioData generateAudioData(AudioData audioData, String toSpeak) throws I
     }
 
     // filter out breaking chars
-    if (c.replaceChars == null) {
+    if (config.replaceChars == null) {
       // if not user defined - escape double quotes to not affect templates
-      c.replaceChars = new HashMap<>();
-      c.replaceChars.put("\'", "\'\'");
+      config.replaceChars = new HashMap<>();
+      config.replaceChars.put("\'", "\'\'");
     }
-    for (String target : c.replaceChars.keySet()) {
-      toSpeak = toSpeak.replace(target, c.replaceChars.get(target));
+    for (String target : config.replaceChars.keySet()) {
+      toSpeak = toSpeak.replace(target, config.replaceChars.get(target));
     }
 
     Platform platform = Runtime.getPlatform();
@@ -209,7 +207,7 @@ public void loadVoices() {
     // FIXME this is not right - it should be based on speechType not OS
     // speechType should be "set" based on OS and user preference
     if (platform.isWindows()) {
-      
+
       try {
 
         List<String> args = new ArrayList<>();
@@ -273,9 +271,9 @@ public void loadVoices() {
       }
     }
     // let apply config add and set the voices
-//    else if (platform.isLinux()) {
-//      addVoice("Linus", "male", "en-US", "festival");
-//    }
+    // else if (platform.isLinux()) {
+    // addVoice("Linus", "male", "en-US", "festival");
+    // }
   }
 
   public void removeExt(boolean b) {
@@ -291,8 +289,7 @@ public boolean setEspeak() {
       return false;
     }
 
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "Espeak";
+    config.speechType = "Espeak";
     voices.clear();
     addVoice("espeak", "male", "en-US", "espeak");
     removeExt(false);
@@ -310,10 +307,9 @@ public boolean setFestival() {
       return false;
     }
 
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
     voices.clear();
     addVoice("Linus", "male", "en-US", "festival");
-    c.speechType = "Festival";
+    config.speechType = "Festival";
     removeExt(false);
     setTtsHack(false);
     setTtsCommand("echo \"{text}\" | text2wave -o {filename}");
@@ -330,9 +326,8 @@ public boolean setPico2Wav() {
       error("pico2wave only supported on Linux");
       return false;
     }
-    
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "Pico2Wav";
+
+    config.speechType = "Pico2Wav";
     removeExt(false);
     setTtsHack(false);
 
@@ -343,13 +338,13 @@ public boolean setPico2Wav() {
     addVoice("es-ES", "female", "es-ES", "pico2wav");
     addVoice("fr-FR", "female", "fr-FR", "pico2wav");
     addVoice("it-IT", "female", "it-IT", "pico2wav");
-    
+
     if (voice == null) {
       setVoice(getLocale().getTag());
     }
 
     setTtsCommand("pico2wave -l {voice_name} -w {filename} \"{text}\" ");
- 
+
     broadcastState();
     return true;
   }
@@ -363,19 +358,19 @@ public boolean setPico2Wav() {
    * @param replace
    */
   public void addFilter(String target, String replace) {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    if (c.replaceChars == null) {
-      c.replaceChars = new HashMap<>();
+
+    if (config.replaceChars == null) {
+      config.replaceChars = new HashMap<>();
     }
-    c.replaceChars.put(target, replace);
+    config.replaceChars.put(target, replace);
   }
 
   /**
    * @return setMimic sets the Windows mimic template
    */
   public boolean setMimic() {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "Mimic";
+
+    config.speechType = "Mimic";
     removeExt(false);
     setTtsHack(false);
     if (Runtime.getPlatform().isWindows()) {
@@ -404,8 +399,8 @@ public String setSpeechType(String speechType) {
   }
 
   public String getSpeechType() {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    return c.speechType;
+
+    return config.speechType;
   }
 
   /**
@@ -418,8 +413,8 @@ public boolean setMsSpeech() {
       error("microsoft speech is only supported on Windows");
       return false;
     }
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "MsSpeech";
+
+    config.speechType = "MsSpeech";
 
     removeExt(false);
     setTtsHack(false);
@@ -438,8 +433,8 @@ public boolean setMsSpeech() {
    * @return setSay sets the Mac say template
    */
   public boolean setSay() {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "Say";
+
+    config.speechType = "Say";
     removeExt(false);
     setTtsHack(false);
     setTtsCommand("/usr/bin/say -v {voice_name} --data-format=LEF32@22050 -o {filename} \"{text}\"");
@@ -455,8 +450,8 @@ public boolean setSay() {
    * 
    */
   public boolean setTts() {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    c.speechType = "Tts";
+
+    config.speechType = "Tts";
     removeExt(false);
     setTtsHack(true);
     setTtsCommand("\"" + ttsPath + "\" -f 9 -v {voice} -o {filename} -t \"{text}\"");
@@ -473,8 +468,8 @@ public boolean setTts() {
    * 
    */
   public void setTtsCommand(String ttsCommand) {
-    LocalSpeechConfig c = (LocalSpeechConfig) config;
-    info("LocalSpeech speechType %s template is now: %s", c.speechType, ttsCommand);
+
+    info("LocalSpeech speechType %s template is now: %s", config.speechType, ttsCommand);
     this.ttsCommand = ttsCommand;
   }
 
@@ -492,26 +487,26 @@ public void setTtsHack(boolean b) {
   public void setTtsPath(String ttsPath) {
     this.ttsPath = ttsPath;
   }
-  
+
   public boolean isExecutableAvailable(String executableName) {
     ProcessBuilder processBuilder = new ProcessBuilder();
     String command = "";
     boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
     if (isWindows) {
-        command = "where " + executableName;
+      command = "where " + executableName;
     } else {
-        command = "which " + executableName;
+      command = "which " + executableName;
     }
     processBuilder.command("sh", "-c", command);
     try {
-        Process process = processBuilder.start();
-        process.waitFor();
-        return process.exitValue() == 0;
+      Process process = processBuilder.start();
+      process.waitFor();
+      return process.exitValue() == 0;
     } catch (IOException | InterruptedException e) {
-        e.printStackTrace();
-        return false;
+      e.printStackTrace();
+      return false;
     }
-}
+  }
 
   public LocalSpeechConfig apply(LocalSpeechConfig config) {
     super.apply(config);
diff --git a/src/main/java/org/myrobotlab/service/RemoteSpeech.java b/src/main/java/org/myrobotlab/service/RemoteSpeech.java
new file mode 100644
index 0000000000..6da50b45a9
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/RemoteSpeech.java
@@ -0,0 +1,77 @@
+package org.myrobotlab.service;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.myrobotlab.io.FileIO;
+import org.myrobotlab.logging.Level;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
+import org.myrobotlab.service.config.HttpClientConfig;
+import org.myrobotlab.service.config.RemoteSpeechConfig;
+import org.myrobotlab.service.data.AudioData;
+import org.slf4j.Logger;
+
+public class RemoteSpeech extends AbstractSpeechSynthesis<RemoteSpeechConfig> {
+
+  private static final long serialVersionUID = 1L;
+
+  public final static Logger log = LoggerFactory.getLogger(RemoteSpeech.class);
+
+  public transient HttpClient<HttpClientConfig> http = null;
+  
+  protected Set<String> types = new HashSet<>(Arrays.asList("ModzillaTTS"));
+
+  // http://localhost:5002/api/tts?text=Hello%20I%20am%20a%20speech%20synthesis%20system%20version%202
+  public RemoteSpeech(String n, String id) {
+    super(n, id);
+  }
+  
+  @SuppressWarnings({ "unchecked", "rawtypes" })
+  @Override
+  public void startService() {
+    super.startService();
+    http = (HttpClient)startPeer("http");
+  }
+
+  public static void main(String[] args) {
+    try {
+
+      LoggingFactory.init(Level.INFO);
+
+      Runtime.start("webgui", "WebGui");
+      Runtime.start("python", "Python");
+      Runtime.start("mouth", "RemoteSpeech");
+
+    } catch (Exception e) {
+      log.error("main threw", e);
+    }
+  }
+
+  @Override
+  public AudioData generateAudioData(AudioData audioData, String toSpeak) throws Exception {
+
+    try {
+      // IF GET must url encode .. use replace tags like {urlEncodedText}
+      String localFileName = getLocalFileName(toSpeak);
+      // merge template with text and/or config
+      String url = config.url.replace("{text}", URLEncoder.encode(toSpeak, StandardCharsets.UTF_8.toString()));
+      byte[] bytes = http.getBytes(url);
+      FileIO.toFile(localFileName, bytes);
+      return new AudioData(localFileName);
+    } catch (Exception e) {
+      error(e);
+    }
+
+    return null;
+  }
+
+  @Override
+  public void loadVoices() throws Exception {
+    addVoice("default", null, null, "remote");
+  }
+}
diff --git a/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java b/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
new file mode 100644
index 0000000000..c7b48009b1
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
@@ -0,0 +1,21 @@
+package org.myrobotlab.service.config;
+
+import org.myrobotlab.framework.Plan;
+
+public class RemoteSpeechConfig extends SpeechSynthesisConfig {
+
+  public String verb = "GET";
+
+  public String url = "http://localhost:5002/api/tts?text={text}";
+
+  public String template = null;
+  
+  public String speechType = "ModzillaTTS";
+
+  public Plan getDefault(Plan plan, String name) {
+    super.getDefault(plan, name);
+    addDefaultPeerConfig(plan, name, "http", "HttpClient", true);
+    return plan;
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/service/meta/RemoteSpeechMeta.java b/src/main/java/org/myrobotlab/service/meta/RemoteSpeechMeta.java
new file mode 100644
index 0000000000..a2b2b7f007
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/meta/RemoteSpeechMeta.java
@@ -0,0 +1,38 @@
+package org.myrobotlab.service.meta;
+
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.meta.abstracts.MetaData;
+import org.slf4j.Logger;
+
+public class RemoteSpeechMeta extends MetaData {
+  private static final long serialVersionUID = 1L;
+  public final static Logger log = LoggerFactory.getLogger(RemoteSpeechMeta.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 RemoteSpeechMeta() {
+
+    // add a cool description
+    addDescription("used as a general template");
+
+    // false will prevent it being seen in the ui
+    setAvailable(true);
+
+    // add dependencies if necessary
+    // addDependency("com.twelvemonkeys.common", "common-lang", "3.1.1");
+
+    setAvailable(false);
+
+    // add it to one or many categories
+    addCategory("general");
+
+    // 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/RemoteSpeechGui.js b/src/main/resources/resource/WebGui/app/service/js/RemoteSpeechGui.js
new file mode 100644
index 0000000000..18ed39bca5
--- /dev/null
+++ b/src/main/resources/resource/WebGui/app/service/js/RemoteSpeechGui.js
@@ -0,0 +1,40 @@
+angular.module("mrlapp.service.RemoteSpeechGui", []).controller("RemoteSpeechGuiCtrl", [
+  "$scope",
+  "mrl",
+  function ($scope, mrl) {
+    console.info("RemoteSpeechGuiCtrl")
+    var _self = this
+    var msg = this.msg
+
+    this.updateState = function (service) {
+      $scope.service = service
+      $scope.$apply()
+    }
+
+    this.onMsg = function (inMsg) {
+      switch (inMsg.method) {
+        case "onState":
+          _self.updateState(inMsg.data[0])
+          break
+        default:
+          console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
+          break
+      }
+    }
+
+    $scope.setType = function (type) {
+      msg.send("set" + type)
+    }
+
+    $scope.speak = function (text) {
+      msg.send("speak", text)
+    }
+
+    $scope.setVoice = function () {
+      console.log($scope.service.voice.name)
+      msg.send("setVoice", $scope.service.voice.name)
+    }
+
+    msg.subscribe(this)
+  },
+])
diff --git a/src/main/resources/resource/WebGui/app/service/views/IntroGui.html b/src/main/resources/resource/WebGui/app/service/views/IntroGui.html
index b85d70d473..0c9fd3d99a 100644
--- a/src/main/resources/resource/WebGui/app/service/views/IntroGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/IntroGui.html
@@ -104,7 +104,6 @@
                 </tr>
                 <tr>
                     <td>
-                        <img width="600" src="http://myrobotlab.org/sites/default/files/users/user3images/Serial_1.png"/>
                     </td>
                 </tr>
             </table>
diff --git a/src/main/resources/resource/WebGui/app/service/views/RemoteSpeechGui.html b/src/main/resources/resource/WebGui/app/service/views/RemoteSpeechGui.html
new file mode 100644
index 0000000000..3d8cbdae9b
--- /dev/null
+++ b/src/main/resources/resource/WebGui/app/service/views/RemoteSpeechGui.html
@@ -0,0 +1,31 @@
+<div class="row">
+  <div class="col-md-4">
+    <br />
+    <br />
+    <b>type</b>
+    <br />
+    {{service.config.speechType}}
+
+    <select ng-model="service.config.speechType" ng-change="setType(service.config.speechType)" class="form-control" title="select a type of local voice control">
+      <option ng-repeat="type in service.types" ng-value="{{type}}">{{type}}</option>
+    </select>
+    <div ng-show="service.config.speechType === 'Pico2Wav'">
+      <b>Install pico2wave</b>
+      <br />
+      https://doc.ubuntu-fr.org/svoxpico
+      <br />
+      <pre>sudo apt install libttspico-utils</pre>
+    </div>
+    <br />
+    <b>voice</b>
+    <br />
+    <select ng-model="service.voice.name" ng-change="setVoice(voice)" class="form-control" title="select a voice">
+      <option ng-repeat="(key, voice) in service.voices" ng-value="{{voice.name}}">{{voice.name}}</option>
+    </select>
+    <br />
+    <textarea type="text" class="form-control" placeholder="type here" ng-model="text" />
+    <br />
+    <button class="btn btn-default" ng-click="speak(text)">speak</button>
+    <button class="btn btn-default" ng-click="text=''">clear</button>
+  </div>
+</div>

From 8924641d0874e037185a17853c6ae8dd70eaa763 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Fri, 23 Feb 2024 22:00:02 -0800
Subject: [PATCH 093/131] updates

---
 .../java/org/myrobotlab/service/InMoov2.java  | 374 +++++++++++++-----
 .../java/org/myrobotlab/service/Vertx.java    |   6 +-
 .../java/org/myrobotlab/service/WebGui.java   |  34 +-
 3 files changed, 297 insertions(+), 117 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index c496da1a61..2f2b513e87 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -14,7 +14,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.commons.io.FilenameUtils;
 import org.myrobotlab.framework.Message;
@@ -23,6 +22,7 @@
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
+import org.myrobotlab.framework.StaticType;
 import org.myrobotlab.framework.Status;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.io.FileIO;
@@ -37,6 +37,7 @@
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
+import org.myrobotlab.service.config.OpenCVConfig;
 import org.myrobotlab.service.data.JoystickData;
 import org.myrobotlab.service.data.Locale;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -45,66 +46,14 @@
 import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
 import org.myrobotlab.service.interfaces.ServoControl;
 import org.myrobotlab.service.interfaces.Simulator;
-import org.myrobotlab.service.interfaces.SpeechListener;
 import org.myrobotlab.service.interfaces.SpeechRecognizer;
+import org.myrobotlab.service.interfaces.SpeechSynthesis;
 import org.myrobotlab.service.interfaces.TextListener;
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
 
-public class InMoov2 extends Service<InMoov2Config>
-    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
-
-  public class Heart implements Runnable {
-    private final ReentrantLock lock = new ReentrantLock();
-    private Thread thread;
-
-    @Override
-    public void run() {
-      if (lock.tryLock()) {
-        try {
-          while (!Thread.currentThread().isInterrupted()) {
-            invoke("publishHeartbeat");
-            Thread.sleep(config.heartbeatInterval);
-          }
-        } catch (InterruptedException ignored) {
-          Thread.currentThread().interrupt();
-        } finally {
-          lock.unlock();
-          log.info("heart stopping");
-          thread = null;
-        }
-      }
-    }
-
-    public void start() {
-      if (thread == null) {
-        log.info("starting heart");
-        thread = new Thread(this, String.format("%s-heart", getName()));
-        thread.start();
-        config.heartbeat = true;
-      } else {
-        log.info("heart already started");
-      }
-    }
-
-    public void stop() {
-      if (thread != null) {
-        thread.interrupt();
-        config.heartbeat = false;
-      } else {
-        log.info("heart already stopped");
-      }
-    }
-  }
-
-  public static class Heartbeat {
-    public long count = 0;
-    public long ts = System.currentTimeMillis();
-
-    public Heartbeat(InMoov2 inmoov) {
-      this.count = inmoov.heartbeatCount;
-    }
-  }
+public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, TextListener, TextPublisher,
+    JoystickListener, LocaleProvider, IKJointAngleListener {
 
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
@@ -114,7 +63,7 @@ public Heartbeat(InMoov2 inmoov) {
    * This method will load a python file into the python interpreter.
    * 
    * @param file
-   *          file to load
+   *             file to load
    * @return success/failure
    */
   @Deprecated /* use execScript - this doesn't handle resources correctly */
@@ -172,6 +121,77 @@ public static void main(String[] args) {
         return;
       }
 
+      OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {
+      });
+      ocvConfig.flip = true;
+      i01.setPeerConfigValue("opencv", "flip", true);
+      // i01.savePeerConfig("", null);
+
+      // Runtime.startConfig("default");
+
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
+      // "WebGui",
+      // "intro", "Intro", "python", "Python" });
+
+      Runtime.start("python", "Python");
+      // Runtime.start("ros", "Ros");
+      Runtime.start("intro", "Intro");
+      // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+      // i01.startPeer("simulator");
+      // Runtime.startConfig("i01-05");
+      // Runtime.startConfig("pir-01");
+
+      // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly");
+      // i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+
+      // polly.speakBlocking("Hi, to be or not to be that is the question,
+      // wheather to take arms against a see of trouble, and by aposing them end
+      // them, to sleep, to die");
+      // i01.startPeer("mouth");
+      // i01.speakBlocking("Hi, to be or not to be that is the question,
+      // wheather to take arms against a see of trouble, and by aposing them end
+      // them, to sleep, to die");
+
+      Runtime.start("python", "Python");
+
+      // i01.startSimulator();
+      Plan plan = Runtime.load("webgui", "WebGui");
+      // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui");
+      // webgui.autoStartBrowser = false;
+      Runtime.startConfig("webgui");
+      Runtime.start("webgui", "WebGui");
+
+      Random random = (Random) Runtime.start("random", "Random");
+
+      random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
+
+      random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
+      random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
+
+      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
+          8.0, 25.0);
+      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
+          8.0, 25.0);
+
+      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
+          130.0, 175.0);
+      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
+          5.0, 40.0);
+
+      random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
+      random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
+
+      random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0);
+      random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0);
+
+      random.save();
+
+      // i01.startChatBot();
+      //
+      // i01.startAll("COM3", "COM4");
+      Runtime.start("python", "Python");
+
     } catch (Exception e) {
       log.error("main threw", e);
     }
@@ -246,7 +266,8 @@ public static void main(String[] args) {
 
   public InMoov2(String n, String id) {
     super(n, id);
-    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
+        "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -260,7 +281,8 @@ public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
 
-      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
+      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT",
+          "fi-FI", "pt-PT", "tr-TR");
 
       if (c.locale != null) {
         setLocale(c.locale);
@@ -635,9 +657,9 @@ public void displayFullScreen(String src) {
       error("could not display picture %s", src);
     }
   }
-  
+
   public void enableRandomHead() {
-    Random random = (Random)getPeer("random");
+    Random random = (Random) getPeer("random");
     if (random != null) {
       random.disableAll();
       random.enable(String.format("%s.setHeadSpeed", getName()));
@@ -645,12 +667,12 @@ public void enableRandomHead() {
       random.enable();
     }
   }
-  
+
   public void disableRandom() {
-    Random random = (Random)getPeer("random");
+    Random random = (Random) getPeer("random");
     if (random != null) {
       random.disable();
-    }    
+    }
   }
 
   public void enable() {
@@ -683,7 +705,7 @@ public boolean exec(String pythonCode) {
    * This method will try to launch a python command with error handling
    * 
    * @param gesture
-   *          the gesture
+   *                the gesture
    * @return gesture result
    */
   public String execGesture(String gesture) {
@@ -718,7 +740,7 @@ public void execScript() {
    * a filesystem file :P
    * 
    * @param someScriptName
-   *          execute a resource script
+   *                       execute a resource script
    * @return success or failure
    */
   public void execScript(String someScriptName) {
@@ -796,11 +818,18 @@ public InMoov2Head getHead() {
    */
   public Long getLastActivityTime() {
     Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
-    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
-    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
-    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
-    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
-    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime()
+        : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime()
+        : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null
+        ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime()
+        : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null
+        ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime()
+        : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime()
+        : null;
 
     Long lastActivityTime = null;
 
@@ -960,7 +989,7 @@ public void loadGestures() {
    * file should contain 1 method definition that is the same as the filename.
    * 
    * @param directory
-   *          - the directory that contains the gesture python files.
+   *                  - the directory that contains the gesture python files.
    * @return true/false
    */
   public boolean loadGestures(String directory) {
@@ -1059,7 +1088,8 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D
     moveHand(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
+      Double wrist) {
     invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1111,8 +1141,10 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring
     moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
-    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
+  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
+      Integer wrist) {
+    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
+        (double) wrist);
   }
 
   public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) {
@@ -1123,8 +1155,10 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin
     moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
-    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
+  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
+      Integer wrist) {
+    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
+        (double) wrist);
   }
 
   public void moveTorso(Double topStom, Double midStom, Double lowStom) {
@@ -1153,7 +1187,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
    * comes in from runtime which owns the config list
    * 
    * @param configList
-   *          list of configs
+   *                   list of configs
    */
   public void onConfigList(List<String> configList) {
     this.configList = configList;
@@ -1176,7 +1210,7 @@ public void onEndSpeaking(String utterance) {
    * including lower level logs that do not propegate as statuses
    * 
    * @param log
-   *          - flushed log from Log service
+   *            - flushed log from Log service
    */
   public void onErrors(List<LogEntry> log) {
     errors.addAll(log);
@@ -1221,6 +1255,23 @@ public void onJoystickInput(JoystickData input) throws Exception {
     invoke("publishEvent", "joystick");
   }
 
+  /**
+   * Centralized logging system will have all logging from all services,
+   * including lower level logs that do not propegate as statuses
+   * 
+   * @param log
+   *            - flushed log from Log service
+   */
+  public void onLogEvents(List<LogEntry> log) {
+    // scan for warn or errors
+    for (LogEntry entry : log) {
+      if ("ERROR".equals(entry.level) && errors.size() < 100) {
+        errors.add(entry);
+        // invoke("publishError", entry);
+      }
+    }
+  }
+
   public String onNewState(String state) {
     log.error("onNewState {}", state);
 
@@ -1254,7 +1305,8 @@ public void onPeak(double volume) {
 
   public void onPirOff() {
     log.info("onPirOff");
-    setPredicate(String.format("%s.pir_off", getName()), System.currentTimeMillis());
+    setPredicate("pir", true);
+    setPredicate("pir.off", System.currentTimeMillis());
     processMessage("onPirOff");
   }
 
@@ -1264,7 +1316,8 @@ public void onPirOff() {
    */
   public void onPirOn() {
     log.info("onPirOn");
-    setPredicate(String.format("%s.pir_on", getName()), System.currentTimeMillis());
+    setPredicate("pir", false);
+    setPredicate("pir.on", System.currentTimeMillis());
     processMessage("onPirOn");
   }
 
@@ -1415,7 +1468,7 @@ public void processMessage(String method) {
    * @param method
    * @param data
    */
-  public void processMessage(String method, Object ... data) {
+  public void processMessage(String method, Object... data) {
     // User processing should not occur until after boot has completed
     if (!state.equals("boot")) {
       // FIXME - this needs to be in config
@@ -1525,7 +1578,8 @@ public Heartbeat publishHeartbeat() {
       }
 
       // interval event firing
-      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+      if (config.stateRandomInterval != null
+          && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
         // fsm.fire("random");
         stateLastRandomTime = System.currentTimeMillis();
       }
@@ -1577,7 +1631,8 @@ public Message publishMessage(Message msg) {
     return msg;
   }
 
-  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) {
+  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder,
+      Double omoplate) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("bicep", bicep);
     map.put("rotate", rotate);
@@ -1591,7 +1646,8 @@ public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double
     return map;
   }
 
-  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure,
+      Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Object> map = new HashMap<>();
     map.put("which", which);
     map.put("thumb", thumb);
@@ -1608,7 +1664,8 @@ public HashMap<String, Object> publishMoveHand(String which, Double thumb, Doubl
     return map;
   }
 
-  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) {
+  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw,
+      Double rollNeck) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("neck", neck);
     map.put("rothead", rothead);
@@ -1628,7 +1685,8 @@ public HashMap<String, Double> publishMoveLeftArm(Double bicep, Double rotate, D
     return map;
   }
 
-  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger,
+      Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1648,7 +1706,8 @@ public HashMap<String, Double> publishMoveRightArm(Double bicep, Double rotate,
     return map;
   }
 
-  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger,
+      Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1734,7 +1793,7 @@ public StateChange publishStateChange(StateChange stateChange) {
 
     lastState = state;
     state = stateChange.state;
-    
+
     setPredicate(String.format("%s.end", lastState), System.currentTimeMillis());
     setPredicate(String.format("%s.start", state), System.currentTimeMillis());
 
@@ -1830,7 +1889,8 @@ public void setAutoDisable(Boolean param) {
   }
 
   @Override
-  public void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+  public void setConfigValue(String fieldname, Object value)
+      throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
     super.setConfigValue(fieldname, value);
     setPredicate(fieldname, value);
   }
@@ -1839,7 +1899,8 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
+      Double wrist) {
     InMoov2Hand hand = getHand(which);
     if (hand == null) {
       warn("%s hand not started", which);
@@ -1849,12 +1910,14 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger,
+      Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
+      Double wrist) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1870,7 +1933,8 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null);
   }
 
-  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
+  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
+      Double rollNeckSpeed) {
     sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1894,7 +1958,8 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl
   }
 
   @Deprecated
-  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
+  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
+      Double rollNeckSpeed) {
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1906,12 +1971,15 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int
     setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
+      Double wrist) {
     setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
-    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
+  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
+      Integer wrist) {
+    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
+        (double) wrist);
   }
 
   @Override
@@ -1966,12 +2034,15 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In
     setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
+  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
+      Double wrist) {
     setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
-    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
+  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
+      Integer wrist) {
+    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
+        (double) wrist);
   }
 
   public boolean setSpeechType(String speechType) {
@@ -2071,6 +2142,107 @@ public void speakBlocking(String format, Object... args) {
     }
   }
 
+  @Deprecated /* use startPeers */
+  public void startAll() throws Exception {
+    startAll(null, null);
+  }
+
+  @Deprecated /* use startPeers */
+  public void startAll(String leftPort, String rightPort) throws Exception {
+    startMouth();
+    startChatBot();
+
+    // startHeadTracking();
+    // startEyesTracking();
+    // startOpenCV();
+    startEar();
+
+    startServos();
+    // startMouthControl(head.jaw, mouth);
+
+    speakBlocking(get("STARTINGSEQUENCE"));
+  }
+
+  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
+  public void startBrain() {
+    startChatBot();
+  }
+
+  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
+  public ProgramAB startChatBot() {
+
+    try {
+      chatBot = (ProgramAB) startPeer("chatBot");
+
+      if (locale != null) {
+        chatBot.setCurrentBotName(locale.getTag());
+      }
+
+      // FIXME remove get en.properties stuff
+      speakBlocking(get("CHATBOTACTIVATED"));
+
+      chatBot.attachTextPublisher(ear);
+
+      // this.attach(chatBot); FIXME - attach as a TextPublisher - then
+      // re-publish
+      // FIXME - deal with language
+      // speakBlocking(get("CHATBOTACTIVATED"));
+      chatBot.repetitionCount(10);
+      // chatBot.setPath(getResourceDir() + fs + "chatbot");
+      // chatBot.setPath(getDataDir() + "ProgramAB");
+      chatBot.startSession("default", locale.getTag());
+      // reset some parameters to default...
+      chatBot.setPredicate("topic", "default");
+      chatBot.setPredicate("questionfirstinit", "");
+      chatBot.setPredicate("tmpname", "");
+      chatBot.setPredicate("null", "");
+      // load last user session
+      if (!chatBot.getPredicate("name").isEmpty()) {
+        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown")
+            || chatBot.getPredicate("lastUsername").equals("default")) {
+          chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
+        }
+      }
+      chatBot.setPredicate("parameterHowDoYouDo", "");
+      chatBot.savePredicates();
+      htmlFilter = (HtmlFilter) startPeer("htmlFilter");// Runtime.start("htmlFilter",
+      // "HtmlFilter");
+      chatBot.attachTextListener(htmlFilter);
+      htmlFilter.attachTextListener((TextListener) getPeer("mouth"));
+      chatBot.attachTextListener(this);
+      // start session based on last recognized person
+      // if (!chatBot.getPredicate("default", "lastUsername").isEmpty() &&
+      // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
+      // chatBot.startSession(chatBot.getPredicate("lastUsername"));
+      // }
+      if (chatBot.getPredicate("default", "firstinit").isEmpty()
+          || chatBot.getPredicate("default", "firstinit").equals("unknown")
+          || chatBot.getPredicate("default", "firstinit").equals("started")) {
+        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
+        invoke("publishEvent", "FIRST INIT");
+      } else {
+        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
+        invoke("publishEvent", "WAKE UP");
+      }
+    } catch (Exception e) {
+      speak("could not load chatBot");
+      error(e.getMessage());
+      speak(e.getMessage());
+    }
+    broadcastState();
+    return chatBot;
+  }
+
+  @Deprecated /* use startPeer */
+  public SpeechRecognizer startEar() {
+
+    ear = (SpeechRecognizer) startPeer("ear");
+    ear.attachSpeechSynthesis((SpeechSynthesis) getPeer("mouth"));
+    ear.attachTextListener(chatBot);
+    broadcastState();
+    return ear;
+  }
+
   public void startedGesture() {
     startedGesture("unknown");
   }
diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java
index d51deeffb1..a47d75a954 100644
--- a/src/main/java/org/myrobotlab/service/Vertx.java
+++ b/src/main/java/org/myrobotlab/service/Vertx.java
@@ -78,13 +78,15 @@ public void start() {
      * </pre>
      */
 
-    // vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));
+    // vertx = io.vertx.core.Vertx.vertx(new
+    // VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));
     vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000));
     vertx.deployVerticle(new ApiVerticle(this));
 
     if (config.autoStartBrowser) {
       log.info("auto starting default browser");
-      String startUrl = (String.format((config.ssl) ? "https:" : "http:") + String.format("//localhost:%d/index.html", config.port));
+      String startUrl = (String.format((config.ssl) ? "https:" : "http:")
+          + String.format("//localhost:%d/index.html", config.port));
       BareBonesBrowserLaunch.openURL(startUrl);
     }
     listening = true;
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index e6efa1f28d..21971af537 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -61,7 +61,8 @@
  * services are already APIs - perhaps a data API - same as service without the
  * message wrapper
  */
-public class WebGui extends Service<WebGuiConfig> implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
+public class WebGui extends Service<WebGuiConfig>
+    implements AuthorizationProvider, Gateway, Handler, ServiceLifeCycleListener {
 
   public static class LiveVideoStreamHandler implements Handler {
 
@@ -126,7 +127,7 @@ public Panel(String name, int x, int y, int z) {
    * needed to get the api key to select the appropriate api processor
    * 
    * @param uri
-   *          u
+   *            u
    * @return api key
    * 
    */
@@ -269,9 +270,9 @@ public boolean getAutoStartBrowser() {
    * String broadcast to specific client
    * 
    * @param uuid
-   *          u
+   *             u
    * @param str
-   *          s
+   *             s
    * 
    */
   public void broadcast(String uuid, String str) {
@@ -313,7 +314,9 @@ public Config.Builder getNettosphereConfig() {
         // cert.privateKey()).build();
 
         SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate();
-        SslContext context = SslContextBuilder.forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey()).sslProvider(SslProvider.JDK)
+        SslContext context = SslContextBuilder
+            .forServer(selfSignedCertificate.certificate(), selfSignedCertificate.privateKey())
+            .sslProvider(SslProvider.JDK)
             .clientAuth(ClientAuth.NONE).build();
 
         configBuilder.sslContext(context);
@@ -492,7 +495,8 @@ public void handle(AtmosphereResource r) {
         } else if ((bodyData != null) && log.isDebugEnabled()) {
           logData = bodyData;
         }
-        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(), request.getRequestURI(), logData, uuid);
+        log.debug("-->{} {} {} - [{}] from connection {}", (newPersistentConnection) ? "new" : "", request.getMethod(),
+            request.getRequestURI(), logData, uuid);
       }
 
       // important persistent connections will have associated routes ...
@@ -570,7 +574,8 @@ public void handle(AtmosphereResource r) {
           }
 
           if (msg.containsHop(getId())) {
-            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
+            log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{}", getName(), msg.sender,
+                msg.name, msg.method);
             return;
           }
 
@@ -914,7 +919,7 @@ public void run() {
    * remotely control UI
    * 
    * @param panel
-   *          - the panel which has been moved or resized
+   *              - the panel which has been moved or resized
    */
   public void savePanel(Panel panel) {
     if (panel.name == null) {
@@ -1101,7 +1106,7 @@ public void releaseService() {
    * Default (false) is to use the CDN
    *
    * @param useLocalResources
-   *          - true uses local resources fals uses cdn
+   *                          - true uses local resources fals uses cdn
    */
   public void useLocalResources(boolean useLocalResources) {
     this.useLocalResources = useLocalResources;
@@ -1177,8 +1182,9 @@ public static void main(String[] args) {
 
     try {
 
-      Runtime.main(new String[] { "--log-level", "warn", "-s", "log", "Log", "webgui", "WebGui", "intro", "Intro", "python", "Python" });
-      // Runtime.main(new String[] { "-c", "worky"});
+      // Runtime.main(new String[] { "--log-level", "warn", "-s", "log", "Log",
+      // "webgui", "WebGui", "intro", "Intro", "python", "Python" });
+      Runtime.main(new String[] { "-c", "worky" });
       // Runtime.main(new String[] { "--install" });
 
       boolean done = true;
@@ -1187,8 +1193,7 @@ public static void main(String[] args) {
       }
 
       // Platform.setVirtual(true);
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
-      // "WebGui",
+      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui", "WebGui",
       // "intro", "Intro", "python", "Python", "-c", "dev" });
       // Runtime.startConfig("dev");
 
@@ -1243,7 +1248,8 @@ public static void main(String[] args) {
       arduino.connect("/dev/ttyACM0");
 
       for (int i = 0; i < 1000; ++i) {
-        webgui.display("https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
+        webgui.display(
+            "https://i.kinja-img.com/gawker-media/image/upload/c_scale,f_auto,fl_progressive,q_80,w_800/pytutcxcrfjvuhz2jipa.jpg");
       }
 
       // Runtime.setLogLevel("ERROR");

From 2a3584a96f0522b892398084d3e3719f2eb004ce Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 24 Feb 2024 06:37:54 -0800
Subject: [PATCH 094/131] randomtest update

---
 src/test/java/org/myrobotlab/service/RandomTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java
index f089f2e453..9284a519b4 100644
--- a/src/test/java/org/myrobotlab/service/RandomTest.java
+++ b/src/test/java/org/myrobotlab/service/RandomTest.java
@@ -48,7 +48,7 @@ public void testService() throws Exception {
     assertTrue("should not have method", !random.getKeySet().contains("clock.setInterval"));
 
     random.addRandom(0, 200, "clock", "setInterval", 5000, 10000);
-    random.addRandom(0, 200, "clock", "startClock");
+    random.addRandom(0, 100, "clock", "startClock");
 
     sleep(500);
     assertTrue("clock should be started 1", clock.isClockRunning());

From 72add7baf5e4948457caeeeadb7db315dd862d9d Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 24 Feb 2024 06:39:03 -0800
Subject: [PATCH 095/131] missing service images

---
 src/main/resources/resource/Email.png        | Bin 0 -> 4979 bytes
 src/main/resources/resource/Lloyd.png        | Bin 0 -> 4819 bytes
 src/main/resources/resource/Maven.png        | Bin 0 -> 7858 bytes
 src/main/resources/resource/RemoteSpeech.png | Bin 0 -> 3858 bytes
 src/main/resources/resource/RoboClaw.png     | Bin 0 -> 4405 bytes
 src/main/resources/resource/Slack.png        | Bin 0 -> 8951 bytes
 6 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/main/resources/resource/Email.png
 create mode 100644 src/main/resources/resource/Lloyd.png
 create mode 100644 src/main/resources/resource/Maven.png
 create mode 100644 src/main/resources/resource/RemoteSpeech.png
 create mode 100644 src/main/resources/resource/RoboClaw.png
 create mode 100644 src/main/resources/resource/Slack.png

diff --git a/src/main/resources/resource/Email.png b/src/main/resources/resource/Email.png
new file mode 100644
index 0000000000000000000000000000000000000000..595d625b2161ba6e159c9991784862658d365e1e
GIT binary patch
literal 4979
zcmV-(6O8PMP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00007bV*G`2jK|@
z3n(kZ1Lq+C000SaNLh0L01FcU01FcV0GgZ_00004XF*Lt006O%3;baP000vLNkl<Z
zc-qBT2XI^0mHm?9?8aGpCZ4P}yY||<likGG5<8o+<8_NOa>j|X>YcVM%PN*++3JO&
zs7^{OVs9c?#ZG`7B0z!!NCHG7QAkvR0NB7LcF~@7o3r<Q0F-3OQj$^ZKXc~;A0MCR
zymRh-_ktXEkK^^<{qA9t#o;k@Sz8N>OIB>%y7fEUeGY*@@T=+B8K1)HLvC9ybgohK
zdfbJ3cD?uA`+fI;A8)?(*5hWYbH>u+MsNQhynX#%wOSjQnJIW-`}XaReEI%@f`VU9
z%*@Vax46K?Yr!Sxz$NOCu5iQa8#N{+COq@C5;zfd@<;Yw@23`LFC4vIc)a}>@{M3@
zd;-HGV?(`eZ<EPv&d_z}FX;68$jPb6xen(Lk|Y+SNUeykw;)E`jqpk{uC}=_JU%fc
z7K=k7A|w7SCN}mbv6o~2HSFZcC&NyLJ$~rWp~tpu+x9mPn!t|<_-9s9!0Gmq0s|Nr
z^bv_sjE-H$^@%A=P0wO_b{4eon4FrX_cNHCrI?$=jT^I=pP%DnW^RV=o579wdCbkv
ze{tiZ`OoJ+n*U^O{>BY@Jxyo&`m~iueD8q~I2j)PjMLNqncYPe^4uig8}<_zzdk`E
zCV3*)C#Ntm$p}sJWQNBlVeT11o5@Szfx$6={*miMY>p2`#veqU$j;uF!$%)~jOdu?
z|9W5qcJ1Et4>p%)&e7|E+tY_W%7ek7VV=Oq=s0gBKZz+mrU`uC@HhmuCTxsq#;II2
zu1dNgQrJ+`XhBk?0hx7X*u5i|Bf!%$v;GJWnI{55k?5@lMque{uYJdCvG=>&eZ2qs
z2ZnAVFgkucKw^?#`A*vaUJR4sNQM$N_b_j#8QxyZR<oEQunzAi(rZkR>D_dFj=zW5
zkj0JpkDyYkqaGZArAxnUbvRvY^_hL{ATZ`9z^r3(4uboj4pBuqj58}uP2IFtASNcK
zNu?PcbZN5{3WJA;%~9^m@G(6zhaOjN!viD0jxMWB&cFx#A0$9kXu(E9qt~%Hx(ORD
z%HSLI=S46-rYN!b<ficF$P7<ng5GCcGoyE4j6R#;$*~w78SQ^<>2H=iFaq+H)+Dy_
z%>O|Gtds^-xIQs~+BQ45jY7z@J(whr!2};8Yc*<Jh$_}1hrssqkD`=vA-}$hC&g?g
zQrLNN%!YyZtX9&nH7UDjRa$=e#*BjUb+M^}7mtR8fB(-SP+U^FW6(Fu+vm#!m<q#0
zVsD}xKR+slXK<7hAzKlFQBo#J*a7aHN{Ces0_#H}CGpRWORytOf$@N?@+Dmu8u43^
z5%-OZLu>EH1wlQY3Q5GzH^*Sbhbj0ls(3y_C@YD)obcm6i9kX^;!6zlmkF>Sx#9E<
z;h*-`V1Kd#BLsj~g;rmxc7od^#DUZn0y~Lm$_ZYnO(KM5DtUV`wW{fw#WjR+|K><!
z#*kKT!pXum2pigG>Ll_DiRpP!$%0bJzO#Z)PG*Qdy_}Z);vWV6L|EA0>2!LHn>H~9
zK1d)K!=!@I<^}g|1<vQHXa&c45I%}M33A?61q6oKjFD2CJ>WKra4xq!kl+)L>3aC<
z<D?u@b&#S$X$5yoj6=CCFj$@S`#;?KErus7JpAz_L2-I)QIqq`g^0hsCwSV#p78mG
zEi!F4+B+@q(Td+ng5S{ST8G6=HYvy1>^55cF`ks&J&1olSPO282u(UC4~~VV%TM4^
zf!d$s6fAF)#$K}BI0o6@plCBwFLho9?o<N~mzki{=+51WEwfaTTd(fU|4Q%|E=2sx
zu+QggxAfsL+K>vRIs$f+Krn`gNSA{Y*dxLR2~9izAKhoN_u-MfRp541LfhrWC=p`-
z<@z2{r5tf3+67Xg=582leLVTWp%L^C(lcFscp<I@+?i%v6zO5H*(JB)J9Y8$i(2nQ
z|6Rc+B_}^SHaa%k+U3Ph;#IT~a-1$u(8GOv9t0~=U*L6b-yl|=5#yy}HR$mS@T45Q
z19<*u4OvSFwW*iSmC>;=6iZFuR+k}FNPETjIEIGDkW2vk2K}X(Da&5>_7CALK|59_
z$lx9DF>>COt5*GW5aZF)5znf;lOuP4zZ@6$9Ie)b%;3fomkIo(Hi}j(kC&o{@}Qpq
z=i6%kzTjb-+t3hV3YC<c#c0tx2!ub`Lo+A`)|WusYQg9TB|Rx~GK-$Ky#g1nw)6h4
zly{>_Vc`{GMS(9C%%b!<u}TAt$w4-oWSiWll#dJ5=S*DMGE%^J2Z8SH?wWQ>Ke%W=
z@NY-Af{UU!CxOOj;|aJ4bT73PY<G6k*)uQzg`TQQND(gOw-SIsQf3g!PO3uN%kgfM
z6gF2sDx1y7sq3V37hVaiMNEkr*|nWK*dX{|utjpXJiM(~X=OWfra<s|5NE!)B|_5P
zdH4<jcDud3QfUJh*-X}<h*S^(^5W?Rl*@GR_79Lv`r+iGk6s6Yh@8l10=KmoDuV+a
zx{uu}(^&C-d;>m6Y=od%N8|^2E3&*X**ts^V1SuTg5a4gojtv`HH_2LZZyhdJmA&q
zH#}0O?#^AkX7xjN5NK1X5}1TWdoS`D4A_uL1kN^s3v0lILOEOnlm?tz!b7X;frIR&
z>+Zn|heg<Qz7}SOo9^v{tFPZ5M3Sk}N9y@^^db0${DmOBQb&nSfPMbTW_R@P=UZD9
z*64LxEPU0;C>DhjR7>8wYXWCxWv}d~RdCblx#<tu2xNVG7mnmAusN=VR?fu(W&@7J
z8H`>&>~vkOv*H)~%dt74j%vG;C*fDg%g+-YFKfvMFh~R}PA`>HB}7f8g|ck3J9%sJ
zJN(GlLbJ%)4v*KP-no0vqfMQb_+`tN|J7XyyuWwv)2_aOkC;Uq)Izf{l1*Z4rBwzi
zj1~vF>^*^S-4gz5R+3(2u;W021kdd+Lt;rQk+M*kbfKF_nyfCg>a7q|s&PD{8BJ>I
zLUm&+&j>K<e7RX<P2kAr=x{-4*(*_r$@d<3%vOhyfxo%Zz36uMSDAq|+j?N^wxiQ*
zg`vyFlQLPI3lSt&-)2H&P7`*;im^Ao9*0w;IG)~!Xe!Ayas!{FObu4esAGrG)V0u}
z1PKHcxIQ__Pi-_y|CGDm(V|pcVFZHK2(B;#ZerjGq=7(pcH5vgS)ehPq3JY3XY57?
z5$dEc5J9ueNyPo%>9jcMekaWIEE|D!_PEePYPp!gbk^!R7N))12?VA-D!Gh6xlp+2
zz61)2iZ?I<w*lut+tI;5cUhq|6L3>EA8KP4wHhY0={nJ>>3~XaAW~h>5xEXhDM*N&
z+cbJ8+Emc!^e`CBl$bWC+BH0hTiX<WqevFw3HV!cHg;-t+WSvyp`j;!Xm@!(39fc0
z0oN1oAZRs7-A=2nBGAeXBiab?t<gdt<thyn?K&uR23{Q&*Ba%h5lhiQ#Jb4q7Q2Hd
z!d5>xZwDv71p<Mwb^Q8u8b_yLu~=&FOW@^~UtX*?c9{)U$L+vrrBwt(*+HwVH=#vq
zghJB^xw-?*YCVOHKP!kpi?)NV(|e^FO>OO{lgf}^T*h0DsmWBekiG1*^34AhtDQfy
zx!eD<&G79uA_5arlXPvkXW6o4-@Y#im7!bCz~2U3Z$_)mL;#Hh)Bu^P1C2^O8rpP_
zwrU}1)ljJEy@sx9&_pUQ66{=9UcnOyTFPI|oUjrJC!Y%*J}+zzD!pz`fCN)vglt5O
z;B_|n5sAsk`2z<I{Np{rr<Y34g{Mns<*AJ(FEijRS`*|1ys5pD2Q6vU;aZCZV!4`v
zR=8P(x+WG%i0QRhu0nl_3Jt9)l-FECH7TGXyQyg*m<lW>Or&r(k+9Rs_qg0}Q{FKv
z(NGU31*=d#o&Yt1<72#ySRSONrT+RW!Dp6=xBvEFDSr7@DgkCzG4sG>1iXQOUnAgk
z3Jt28)u<%E6*46%8rvXhXoX1Hf(oeuRTMRit*DW;qP$Lm0%7HX8Z5!Jv?xpo6XjO7
z!vRN64}o{XLmOT2*AEuGq=eh+r6$nF6JRP#&&;5vwsy~5!N+Ijzq&J8`UUq&9Jn_U
zP*$&koK{|{>LlQL)GD;7BH$H`N|Z}mQ6_1jd*mp+)<hvgskjkkVi|<=x}si&^4fX`
z3JP(pNlx}rLZMbeMG32+<kuK2Xs6~-(P@XI-HxJ0E2>)U2>$*QY&TeA=;`a@&#WD1
ziLB9TGVcJt|HzTYPUR}S+|m>*S(Xa!)dWOm*Q0^FUe}`Ifr|)u838Z3ra)m`6Ry@Y
zBC(<su|g$c%i99cikNaGqDqwr6G(CTY6~KTdYmom#EB9k_7|A3EvE~s)694=-iRMW
zc2c8Aga2^@R-{;{9r3pttP$iZl8<s>c$ja|`>E??c5=FURCf?at8CBZHWq+ek&Z>n
z)4;u!gstJlsA<uXP1*@~8;ZmV<kvJKr>YTI6;h;@OYqZ_E^rCdO2^X-d)Wdm#!PLv
z0bB%uI;W?JN(=5J%@rqTUN}xI`mxqP(Cg54Y7*)5Q*`esBdV2Dg*+bI{1J^*AmBqj
zUm%%Ti^1lFf#HylkVhB5N2O-Jy6z$kAAh)lC95*Am`HH1Cu7~=0tjVl6pCArSKSOj
zMI$nV5~P-0Lvl$il8S4w<)V<Lt~~1dDYoTO<OG6VZ_fv}vk=^_QgH8!z<ody+WvZQ
zhp4+fN`Eg@iA7-=I_q#SuZvGySKv2T4|TA8i~w1Oso)zKX1x%f$b3r?5%Hfl36yDN
z-1;Isv?dEnD85A`xTUGcDr;8MwrKm(MU6-)mm;C09&trt#1_;dI=>3hc@>Dw7h&UR
z0#8ZLy-mt(A@W-?XfB{<yqn`E@m?{_%;iLYX5@Wha0jK}j?nCIoaT(<?RfI64y_$j
zJ_7`L-TwbW^bHII2n_M7PbHMCzN)IaYRwzJf8*v}uqL+s|F|`ISiCM5OV(zCTP47+
zwp~T?mHc%DRT5EbaXq50)*>>m8W(aZaXw3g^8z8xXO`ijpakaxW%%U*0=g*?+?z>6
zCWXilp?9wMN$j{vDwOzD_%NWt;YKVvMk*XqBCXm4CzVo1pqwrcphRZ=rwa7B&0()7
zEGk@j*Xr|q_8{i}jrlwYZkYhP&R2hY<oNN&Q*#QBoX@L8_!S{erk5cswFF@)g$PeA
zz_FAvni1;p+*vsmeNai?kMqZ-1R|0QZZji6N^B#Ab{2u#T}CQY^C~Pl*g#4&BP5}n
zI$ImH(LDhIy<{Ppy$AW0V%A_ZUWti~`Hy>VyC*2g-X@=hkY6{@N-oRBi3G8meH3=)
z{PX8>YOyb_00-jo>70-4u_FBbk_=Crp=8`wOF+}Gcugdhth<CI>tnEJ!!0D3rQRh~
zw+C`zFA@E)j#QB1p<_*uw3%VE*<rQ$cVw2llmi1Atv34XxpPn7w`E|*-$xqhf#k_8
zWqu`==OQ$&#=<^JN=bY=rcgS);(Qr?8zIFr=ct;5l6Ut|@@~3}B`Z%;;-A6d)#tHz
zZIDDPJ%<Q{B>VGXYZeip=f201xx!E2{dyz`HPoD03A}@EtLbd2s;b_xYuD~a|0v+W
zBf7F%!(A}*3AN~ejHJGXjnOUh$+Z^MnR46U|6J02!G%)U+fNDjP8Jrey@(|%P7(Ms
z1paIw&QSrlKsj{rCIP0v+oZtOY(IgWC0M+N2<)oGj)Z0ysY$d`?_)A|D=I3h)~#5v
za`B%8Jb3Iou<y~#>h}1pS+>u)vsBB^nf$f+sG2N7%IvPdqIU#juQ&o3MG@f#yz=bL
zp!bW}^|es}0xY3fGE*XN&EYMyh$*nM0xyS4slV#}M5R&{CnYC8|7U@|+YytL_KUbm
zi}dBBK5*eS+I(muI;<cPR5jkCGPos+uQW^6#bC*5vdgOTH-o)j8%>|ZV)2G}zkS{$
z`-D&uZ^^~t9hAg7%JJ;6YsjyX=Ut4v@S_I?{L4p4ZSy;)%H0DGNA{B6DL;;p5{DH0
zzmK?WWQCAaEM8A)tc~%5W@iSRu5la4I_s#Eu2023@5sX5XdzaHQ~BINd%@mnq?NZ6
z|HXjcdK?cu`n}vbee}+}zE62Ij*uEhY3n~oC3iP%NbgX&4IzJTNcIC>OR+8)Pi{@e
z+R$8F$f%%(P7ZmSQZEuWo_Y6j(<hgUmAXCe?|S%aArL&0GO~V_BeCWk%6E=GbyAB*
zj*}WkD4`F^@PoY-cy@0A{%|A@yCVt^kyeV_vRc&3<Y;V?O9ffk|GRwo@<r@9$(eb-
zITCvEsjn6Mtw&-?@=I3=X$MZu$LY8n#AS<+lv@r#VL7grRX|u>hx*3mj~isorSXZ0
z|NV^t{i<WdiWT225{d?s(y!2LTZ+O`A&Pk5Vn}52smhwV`0!JwfA~$~yyqdv&9BHQ
xC_%BX3KcZ0)HgP{3W`eh?LTnf@oy5>{{y*7kSlp(l9K=c002ovPDHLkV1h$!jG+Jk

literal 0
HcmV?d00001

diff --git a/src/main/resources/resource/Lloyd.png b/src/main/resources/resource/Lloyd.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1af6eab7f0b36c8bbc73ef282dbb4168b96ec4c
GIT binary patch
literal 4819
zcmV;^5-jbBP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00009a7bBm000XU
z000XU0RWnu7ytkOAY({UO#lFGbpQZ!pa1~;-2ecCWB>qg=l}rhXaE2(J^%m^kmT%{
z>Hq)|SxH1eRCwB?nrV|9*;(&@XUWW3dy&+weH%$5Y3$iOGt3wpjIo&^!q^0cun7YJ
zpMj47FK)zr4>t(l#WnbbfdJyhHQ>M+?6L6dBTZ}6EvXmvUR~W)mCHHj;YDUGt(hSR
zZgpf-R_4h(&%XShRvi9(j@I0_;$tVSW2_Yc5VTgK1qua9Rp+48_#R3z@e8HO`%3BZ
z998{Rqvy0%<@MpWD$n)s-AbXgA&G6+S-t<k*1|h~0sM5hhn?8(8^kkzJbV7J$F5vB
zhtdWY!k9=kCSFz44>ug2580}Cl=7!B7Olo>^#l`I=`kZ}@?gYxWG#`=ycb8WZrxx0
zI@#_Y0^b|&24Ca&W8eSMSH8;UpL>ewsV2?^f=ETQlrKHRfEryJOS;mLQbXDSAp{|g
z<Dufl8kx|Zn$YT4n`4F}qv`JV`QcCB<o|s4e^ab3{2K5#3TV?jcK6qQ?+=ds=4)TZ
zyMkVSfQU>;UyaU3>K`!xO6%%dO_-Q)>iBU^o;uCy>ME-b9}>q=WlEY!TT>_mIb?n^
zlz^0qM1-W4(5Tn>yMOu-fBHZF$JW8x;;R<;)tL*IkA3--7qCXNyStBbek3iFLJw_J
z=~C^(bFI)yL6Ficf<|kQAb97f*Xzt5n?q~E+Qxb*6m1Uq%g}2Mdk>&w$lYj#a}EJs
z{e_o#=dCx7{mb9)|BgL*<+*FePM_q&{4u(FUGlsjNT}>ogDPdCCrm{Iv<4*#(UTe}
zH<BPYS1{-g=yW=yS%v~a2qTKLVnXJJWPj*eCN!Y6Cd)GBjveFL<xBkN$cZcZcYpso
zbsLwe$@2^uJu!ao*f}RRl*9WD9a;dbFxG++W@l${-jQY*D1{J8vk(vw%D*siNO@f#
z4A+Pt67a#}oI@K!T&p44#CEFHY+O2j77@V*IYba5pfo}0QX5*MD9uKc1_WPtP>Nb?
z321a>V*NqR!<`Oymsh#9@DVCXaA=f()*A0U0$5{^L$*_LrvZ(OH9^VIFu3g<9N_aJ
zSZl3`qo{Nm4ohB?LbM^MsMNVq7^U$_;k-vF#nGelSR1jjwoV8_v!0M7mO@7?+*@O1
zYmX?h)arEvi-@MkJ@r}x368xqC5~dWE(J1-W>yIb!4Oa)h|*|N-s|iiu(P)hMS(4x
z^I4uBwmkvSil8iBMW9Wo{vc%Dp|oOVYMK-C^UTlB<6K~QW0&pD4qh3qo;t<(c8h+0
zKoZB)Y6%Cu0~Q}VBwz^Mapue!P9C3Qb#;Z6)m3a{u~AIn9L{@!_o(18p+JW~5RU@-
zSx$2%#fJb|V<iOfp|blbc7@;(Wx!ZOl^Yh$Q%e%sttQ4OW@o3FYERMa9<bN#pu{oV
zs&nDYJV|Vbql7H-4Dy1RnOWM+I)i?XgEY|H>#(}If;EQuQ|G9)XZi8XH(1-)#6~fq
zG+E&&oJ0GJ*k|B!M8L)|*IQE<T_!*~a+=C)ob%Y&;<Y7p9;M4t;>^ht9G^eIk=a>%
zk@M!;Z?nJOM<lS{+s7Kk)$5m$5I}?wJlk8_9P|#DYE5%=?kJ7+EWz}+eQ%G=yZ5;9
z?rrLgHnn=fsZ%FOa=3T@KApV|&1MUw6-GRbkQ0R*qcwISA(o|tfJi_Doc9>3dF;YD
zqI!#k2M<YV4W4-XapEYVaAh<Zqyx~JPG<+D6;Tvnv?kAT_78eYPt7njTVw9@Ir6o2
zdZIDTaC>Eo`zuQ<t*&FWA<Ht-^$iGt(<e{yr7wJm-A;#pd*gK$?ktcb2}*0c2(i*-
zIFvH851rNqL4pY0c}}00r`>8GO4A#pG?E05rn9%l*7g=zo|6{^gEYljL!87U))Ih0
zUU2)~3WF@;_P@Tt!qNlMBH*0Ext!;oeU|5+d6K2&HIy<arKmUR)ao^Y_x$AMO*S^S
zxO(L(U-{xMarxpU{_W;ZNsM3%MR#X|W)f3NYKW8pw<_jJj<i<fLFf-MiU7UBGsttg
z`@3vzJ}fh`(imf?B{5NC$y{LjV8HG{kJX(H`@M{`NLgB3qMkHJlq1hl8ubQ6ma^aL
z<D92nk3a(At4yyo{dB-PAH2uny+zKRK25zjLoKeO^DeE(a{lov+*@9za0Mb^6mzV|
zuxN(0^$n(54T31P4*KL+thJTIG;tJ_F+R)L-S4rrx6i|!4w-W(6p^(Q-t)p|uJPp4
z&+(JDZ*k}DNBrj3UgM?DJ<s?4_HWAwGnsFyWERCSHUxUvfcNiwNSYQ*HJhj+<;eUD
z*DhUT?&uMgmRCu$yvk7}0|Y4*(nw;mt#!6{wwangg>5uReV}Fy#%TI!&coe3*0wwN
zgFWso+$tS)zst<*EK^fcEG;h5YSihhK48$8!_?Z`eEWTBv1Vgyi#UqQY(GllVU(x@
zh)jfNA!@}04OS<Jjagb=VgH~<mSv-Aci0bzgtE>+gpU?J<iW}c7p{6vKY1MQh0Hm&
zb~-Gqth2G(Wu{rfZEo=98*e}e9GRPAb!C~wg#lAD)2uvLMnrh_7k`Cb98+&i(=P-Y
zCFIUm{uj#RR26qhX@U=gP?SZV(FB3q72I83W_!z06gjmhI+VRj4FnmLuVqK1sW<Ax
zk)?Cc<)ix#xWBqi2!W&1Gc=k_x^V=q;QY1geDPPm&PVUP&DzpkUi`{yEZ+WrcYl0?
zshZ`*S6*Osb)Cfz-lynn(@1Jal#n`4Qv>42vy39osW%$<C?YEy4GD+{sZtoNiSq#s
zqwvP!Mo~y%jW#NZY}vdhO}$=cW_p_T)HDkp-r;-S{~LBTH>uSk2tpj!c;@9-I5K+#
z(`;g+h+G*=Tqe1oE%j!T6Q|E$5MKX>?{o9@8}!o*5Nb({=U@2(C(fSZ{eORhdmnxX
z#-IoUf#+ZOdFD=>;>HjDFN1!c=YH{*nQAq-`B&dXe8CsK{ria0BMoR2A<uKHF&JY&
z!`j1jZoc&{@7=o1();hy-Rp4W>CbWfg_l`gzR!&x{(#<o7h?=AcNp&oMb2Q5;?o{R
z1fva`>+8IJ<8?M~EpYX-Pw~y~{2oOR-h2CJbT`)c@b+ys@7?3f<tx1SOTWTFf56Xv
z@?+Ay4iA=BSX)`a2aiEmTv}wO)4>{Bc}vNH-rU+w@7!M|D+;X9+<N~5{_!9Gfu#q_
zG+S+StIhFKCz&~Wk(sj>IDhFfgMLb$=c67$hVoDf8(Fl`IOj-`m{zL=(^I_s3t!@E
z-~JAtefk-K59Ea-Nov%N&vW64Cz(EXiL)0jkY*VHq1A5Do|z`8H#peaV`h4qr=R;A
zK|%0=Vd<d%<WK)H%pX0<;_5oL?k@4>n{Sb)DM=DzOxXi?=O_jP@-!p(a^on94wu1I
zqf?6_YH@_pia1V2jeKMcX<9OFqlkLFj@B9pLOMuE2R*!VBuRpeqB2q`g)x?_-^06t
zsUx#EAIbnQbmIQbu4*@%tZZ(xvUs1eGA*M*^^Pj+m5$?_A2%5YRPAMvWpwvCXsw8o
z8cJ!r52Lj2eLyLL%k#249zM^ol#xdo;<95)tYUX>mn_SO8*QAg0?yEcsMm1bgGeQ{
z2mqt2CT3E?N@29A`r@*=QB@6<BsJ!a9ie+Lpw}N@jmBD2wxLl(5ny|-S9Vg9{kGO9
zZSYdUFCt;IrPi88t;XisDp_8j>kV@6#|_R97m^`(C6EqM0MabO6*<8NB!p3wS-66t
zD9H1iAVS*jll2d1oIJzPv*%gc+GX$FUHXFoS)P$+rA8NSeZWSySJp;_XVC9a<T=h2
zL|QT3Y)}+AS(cF(InFtP2vX#1K3u~Xi;WTnK2*Kt$AB$G2!Vd@pk$!m!+VFg0`Glk
zuM)_zv}9QVMV^zU19b4DVi`DSx0~$wf;`I*?@5vv>GgT*$2Vv+-{St#BA<EcNt&%T
zNgT7g*X8C9e?WU`ilv1Gu3UeD*jl0}Vs~wg@BZ2UqI+k7mtJ}v9Yy%y5B2RM2Aub3
zV|eN1mswd`<LrftB<*P~KmIuFRtt|Jc+d6g*9j=*=1*|>(ql|dPvf=4r$uQJk>T{&
zvz$J2hPgB6c%pDb*0Q;=PG@tS6Z7+2y7n2&^ie+h)KfHSHFkG)*jj$TnR93P>~qhf
zn{A%B{shad78@%Mxc>ChJoR(0u#*;;C}!+ELlY7(3VLZybovSx&tGFtBGT@FbI-g8
zDE4~;(2A)GS5d%v9=P=4&m%x*kdO4(?e{r-{vyvl{VZ?ZS|F{rdF@-j&6%?oS-5kD
z#e0kNZH=AIKFwpNxOVIm&O0OoOcb#fpy>8!ox8-f^Ov~p9ZlugC<YWRqy0$#ZYTSA
z59}Tc2-*;0AsE9TR84k;HFX9>1@;PQ7y@5{u8OlP%ZZz9j-5Wk;O+P6^atpAlk-=v
zb9;G}-GQT}JqlqzFHw)-A$b7tAkfVnD2+jpdyjRFSc6uAsTzJn#tcZ6maSF<B?P7L
z6+}3~KT&8k3}3(~Vkks`QHnvDGt+Ldv$?^>`Z~S{>}+k)>#VbK|30-SVpvNm35TMN
zI&>oC`z4^o5fKO&4xC{lg+WkYEp`$rALEm1MARdIUs)-Rz*_++*CB$oWr+QM|9pdX
z`yH@`W~<3hZob3H+9uupfOe}@hV7vhi|}b5HDXdo#gIut;bYW+XFph|H_D!PRFy0E
zwAYOY9sypHiv>}NqHxsf2_cF(e)2Tk-X6h~*IMlv=1!bs>E04?6c6#j|AhF`Q5}j*
zVsa8hG_h2Gm(rkZX}Y6#$s>rNe4L3#mQF|#(ljNpmf!xZ-{7@Zze=9wq=Nxp{QQf2
z=iA@p)X5X1gLDE?eDq!86jg|(Czg&R97uw%Fl<#>iIRYlaA?565E++Gkj;=urG{<b
zgnfyilvLlUL@tUQoOhrUd0w!)vx9d9N}ETa*AciLP&hQH7>W{;Lx~XK4;lC*efDFD
z6A=9=rh_0sQC1j_Ao|0^VlBo-JXl>LO;e&cVlYS%QKV@K5=KruB*cp*$55cld{c&q
zO4<P?NEeu>o7AHWNEKejizm%VM&qOj2y)~*!>!6$!RFRx3C)YJ-|rE`v)>z3=1~$^
zpm8X?XhKEq&`fy5T!Vmlg!l;!3^SV=IjIf-W#to)<FP!-kUBX=QD_zD?eDOE(8ZXD
zfM&0sbMN66%R2}3ZG%GMk;%wN(nC!@>Ud)>k;-95nOLb37m^Y7UsjhI58@E-@U~1-
zS{-IYp+@L}R6w{>hF&n-eYnX^27TQiAVeW%;o%NDgMwg6u#5@9KQUqqE7HnY$8^dN
z$T)05YQi+6avl{N1cNsQH_8pV5{xEPrXy03Vl<tDl=Zh3s5e`9T{bq|e#ZN^@02Rn
z>X3;%;%tW^h8B+&S27?IK5)1eQp!|$U|%I1M5>x=Xq46jF@&J-%9JyLppk0qBr=|E
zDODPU%X5qZ9UKM+zI<$EdWQX8k2KA()_$_YWjyp#63}?Gj3+w7?oO(_Q|5u-F~-s-
zqDw-m>trS&(=izqC#veIO1xf4fesEU1(6g)A;(IA@i}RCmzO?sk#BtUizt^vks(D&
zO?y87%vH{xI7&V^DA$WCkKo4V!{4F2uR<8l$kf;ewKhcM2uoX(<jWhIGzS42MR->r
zVZx;q74d-5Of*xIevJrRICX?Gb8WUZ*6DS-G#hoiK-wSB?QHYRwTtZS?y$bOLoJCu
zZaSaf4l-$0TA{QlLAb#IvcFqs2Ax~$yIq1!Na7f4%LQ7O02gXJV$~D>J}`<{6(=H|
zQ^)71HCik^Sj9vZr4+_!d=OS2ZZh3&bL_}0-n&Phf8<c(Lst`0Z8X{#Oq9^DhKEZF
zu)g@ACEfq~e0y!Xmc&QToIB6*{rhBDib(kQBwfaH+eb%gh4b8B+hJ!vXTP7(Znj2}
z)yA;C-Q`C=eVg^o9op@7i9!7rKEu7VF+_2V%a<RcUG&*oyuC%<`?Dd1zkd9MFaPy(
zuYQB1IX%j>q55BmNgWTz{}cR-uIJ9-y`vV#RmCbJ=DioPEF*~{tTm$|w2FD3YE1yf
znsQALLT``P|N75ae(T29f$vYQ|0eSn|LFL&XU?BJdlpkoWYok4d`iq#e9F99jR)?B
z)hB%73*-~t3D`Iy<T*=sKVt3OpDodS_=mt>jn`=d!2`TV{m8dCcJi7s)|AdXk!}CK
tKJwpRrW6Dh*j)X9Z0|3DH-@+VV*oV%{5<qm3_}0_002ovPDHLkV1gNrNSgov

literal 0
HcmV?d00001

diff --git a/src/main/resources/resource/Maven.png b/src/main/resources/resource/Maven.png
new file mode 100644
index 0000000000000000000000000000000000000000..355edb4d2cf36ebb991b3cc2a6ed30af1edb0d66
GIT binary patch
literal 7858
zcmV;j9!=qiP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ000-8dQ@0+Qek%>
zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=TImLoZGMgOr1Edlee9KZmaqaA4Zx({S#RaRF|
zQh!#)i6E17x&t=6!JO{=&wpR{U;L}FMVCwKb=2zlmq#9P@}&FEzw!PCpWom2FMZ$Q
z|G&KMe!dZTDgAb2o8RLnub1CG@Z%a`{rbA=-*;mFo+$i0@n;3PvYzkXFOt{e=k<`k
zCmQGTq>kQxztr_Tk$n$*OZoohpUe6@=>P7Q7aCF8g$tz^PYTJuzx}g<q~Dz8dz}2T
zD4utom(<;RDSS^xKz`ZYk4Nu+AD~}?{NtJXWAuMM`$O>K{Jx(45X<_F5l{Z~jZpsZ
zdir^p_=j@%t|<QcMxLSl&33-O_wIi0`Rs1yN<{0oqF%6nL`VlNPL<xrDnE_i!t?fi
z20z_TZ=`&C^NSw~BSf-tUC3dF8E!c5(SotW91m6RV~j>7^jc3Xj%Z{lkU4(BjV+C|
z$y!Vnk4uS{<IlB(cir)>XQ6ZD9XK=wP8Rsc-~Dm_=*@rU$K7iRQSkj|tXNl6U9${j
zPJcOygoOLeTX_Ng@$*f;|6Tp4R5OPu%$*HRzdol}CHz)f>E$_bT;cK4Lcz8DJpm!&
z+J(i0L<W2fse~GQi?M}396Je`teifHiwuNP;%1RCr&ObBu}58-&$RH4jU@`HI$4?|
zqNJJ{xfyAategw=({iVVW=SQNQfg_XyE4kGspeX0t*r{=W=k!%(rRn1x6x)#J@?XU
zZ@u@?=ZGVLfo0UuMjvC$8QtVUlXFdupS-f-N-M9j>T0X6v8GWzmaSN|X5EI(9e3Km
z#IC#TzQ>*?xC2t0e9Eb(oqoodms-2&=38#P?e;tF{F$|HR{!Sb-^g0{W-Y!<>A3bY
zYrN`O`*DgOI4Q~*8H+iP@umz=&`~+_J>(pfIpxd`kS&$SBFkfu9h5Orm`{l1hM&3n
zEpz`;-dySbQr_Y}%bZi{{u7yVr0#F?_9t1};yHU6`*fjV>J#0^F*A0@ulM}<|33b8
zqQ6e`e?LJ#d2IU2g7dVa-H=bEE2@Oxx>nXzubkFnGkS8(dIk+MTZZmdrO#F>X7$*n
zht@pFozkfGUc#Q;r`_Ax3-)Q{-WW=Z`7E>&0JK}M-E56}pMFax0#?)0Ry;O$Djzv%
zlml;jx-Nz@_lfiRTddG<W$lq$wAShdlYC6Qn9n(FBt^POqUsjczxxgI9C#jVxE4X9
zwffG-_j+xv5pVCDeXMi+0$I4!omS&&BbE`*s16=$K->axPFqksAB@XtE9LfcPwW87
zEd8W~3@{E0>i}A;G-;E0vx;<vOB|L7IC}SnIY#NpTW4bcNJ}@g#MT2j==DUackYyT
z=lo}bV4ypR^Lu!AdRl7`o0`hmD9XJJ%&Yyz3`hsmPr1Zsd0k)e*ne6L?mtkEtZptB
zr=>cl8SFzcSX!?H!Gny6_cjS#v5I%XtX9dzom2$)8VehQW&K9hREG-~2>`W=VEdfG
zF5-)l@Kk7(<RK8L>jb0-b1`;lE0Wwocsp`!wLBfNY^0_lRt#ED#^%~Nov^ko<B{Sm
z#m^kps>$wMeXzb;SQKg=)-aBDtN2lOJfWtJ=qJ7Yvd=rmm9M$OuBx*mAaChZ(S}|2
zvX<teny4}OlLBy}%?26nx+s!`LSg^stO6MoTCE>+5N&ke@8}PHrL}i-d@WVd!Pihs
zj;cF%6nsg$E5W#?Ep8MK5gV6+h_%nXDKJjkMlz&XX)F)2PT6`z5$|#wWGMFCP;Ib5
zyQW>^t@F-wz=d2F41TDzO3m&}6Tq5NR0$l^k)#Hsj2%TMf7Pw}ak$!CEU6q|BoTtq
znhi9e5JZ>M#agFi?c{#!q}L$39)JxZU3sv>hDcu*=phj+Z1j51abL(fR<wEebXMAD
ztp$qcqLW}#FodJJTPx{{N@zf!v#@ZqvZ^bEIm4(2tyBF}p&(_9_~sf(S0E(@7W7p5
zavi<Ea>zY~0l%o;_HF0O;z|Ra5J(cUrw!f;v?dzk6tv1f-vn>AV`BHtpD`#D5N4+}
z`YK$T*zGU!g2!MkrHT02fLoXdB$=9v-afN!)CF>818mrmn{pQ(Pn0u@4XN@<DUvn-
z!7TKgJTR$?V<VlAY1Ts_VLn$5Sos_`PX@`5HfE~``(MJqV`54PjS)5g5A={IV8(Y-
zq#6!4r|`+!^2Y#w7Sth(>5*qj7W)Q^hnQkhq~O@Z;hn1*@&G;Curm|ANqjJCAV)1L
zx=v>g7P{@OU2l<-C&-H^Z+dy<Lah?LRcRLRe{@1eAB@mr=(onVq-pN8-`JL8*$3cY
z#k6m(s+#lqX(7m#xG&wl-s`_Rc}1@8SwnCv4vX?VeGLa*s-i|~o{!Q8ZDIrY+{`NN
z`bv9d*~dsW#@SRL8`qE}fI01kUS|+6Gyn@0CGWj2KZXzBOBaDm;2j*A#2D>-W**kc
zJCjk_M(r7W(<~K$$p~J_k0EUWtm^?DSVG}6On{E1R8Nnkg9xxyg}6p{P)-}reO+)K
z$)x^7+&xT#%?1U@uCV3nrXDj0$$wC}R6RMH+67AkuiOGtP}k6@0?m&p*{ALEaEt5_
zQs8dFmuzFULElT`Uzy4r*&e$|+kvWig7!OsjlJ60gB^v+A!}3^jrR#Aba;76OyG<V
z;Bw%9%zW?geYO^yB9QD9VcEdH6AiZSCy@jQlWiDbA3O)8DmU8kJeewni|K&{pi{Fq
zo4cWIs147F;4sj!gU)eMZ}3_UdoQq#<r>t!@~vVwj?4pmfyNYPa+bV%{R?zVoql%P
z&<B~=!00Fe&tfkY3SJun&GQXk!RAxJbLS<|c3d4G4G|!Go0{<*)ua9_h=ANC{e@=B
zi$0{((q4d1iFsp=PP!Y)x<ne+1+c*iXLC-f{lGO9B%|Dg5U?EL;Z?h|O2LFtR`eoZ
z2=x>10+(GxQ}#~fjW~NPTxe4q8X5C!7jgv4&xPk|nV6>K3%F0c#wqItUMQ&qi2lCV
ztcBoAp$_~d#nxXPCc4|;TJ|gYrXE3G#MhVrSs|}yTO@XzB;UqjfLc5?Ti;8_U2~d5
zxt9u;#c=8FiuxRQNgxW<S}}IyEYZqu_0TGPdRD8f2x;7PZR8+?K@4J&ss+<J?Rkxc
z^Cem0BgjhXl!&jr3mp(7J8{o>2}Wq4aCQ)}k#E92Xats)StW3plALM1QK2KBbg+5S
zNul;e%d-=~w)0dd`keZnxeVTc_nydGADQ!%z>rwpwTZyVF^k5*qO(X0tS&appA{h_
zbdk0b@>61xDsD)aGNn|p_`={8k&&Dksy#xz{O$Y+D#87)^_TCT*-}#BKloOQL5BEy
z6u2n(Zc8Yq(jb`BhL7;9@cv#!_`172J6wSpL)**>vZ>ylUw}k{CymhN6^|kM55U*>
zaFaqw0o_FMxVBa_w3C~~Mu=l0Ev+6%zf%}S10Arxp7pf%zK}f|&iJCK;`S6r4wS8n
zY=d0?34bDL)@>OAMT5ZA!)8lRXWflhXAy}HL=Vg2Z^UL{W}5ics4I|J3QsB-k&;13
z%aUmjJj;0V7z-x=w(moIS)hNAPsvTNOUr>hIgzH0^sFYfU=o@T&fwevq;d*NT353m
z_%Dr9wjHEkfmX_Lty3NfK)tAOx^bWS#)cgr%yR)inVd(J5=cc3sZuzG4)N%NkfArk
z!g5I03;?O>;Bwd>DQ0N!CFM@T@r=E&go{ae<w<B|#o7hPI6|%tr|B+U2OJ%5i_(Xf
z#y+&iH*Wpz@%WF$gF4`4o)K~sU_xR-=^i9@l2j8}8_6rV2!$>Lh$UEsd{@PeR{`YN
z7_~@s$Pp&uvG=><0A`~dU3>h9Y}?Y$A<ykssB$Qp8`EFn@RdoqjE@%xky7MR2UrGV
zk9aengJ2^Gr;r{^4*G44=>nCny8~(#b>L-dbos(UN37?>2x12)tP?nSQ&XU76Y;u~
zMbGdc{C7(pRm2X7nSbtq&IV)KRbNxb^skP@C7|7!<O0UeAh@@R4wJ=0M4mFHKe_@j
z5Jj?WB_}$Kt^cqXs2Y3}u3_=)#O<KK0EBz01q_GfC%zg*in9ht0Evk^_YfhJCc6@1
zsT{!tL76UZf&1!fxHVAZAwWla>Y~IsX}o>ueXT9BWB?bP!?_b>R;Y|DHx4^L@H09(
zG*`$fa2(@L<g0Y*`iXrd8bVMxT=VITxoZ&O6#0cp(E)44GFjUaxoWCT^DNIPsUPXt
zS)cQ}pgvA9gPmn7pR59kXh)=nJh@q~IB8n^KhzrDAqAw;BVbEh-qNXcf3>wqreBkM
zB*-Qo3tfFZSERD345;FsB{L_$FG?HqcoNnD;33G6bMb~SK<wOiGQ_rkfm*061ARns
zWb`UZQgeC~0y+iKX~>h+z&tuXkl&Nlr~wNmH^IK74xd!UxlKyKv0n{!LorD^%4`n{
z0nkD+rmvw!V_e+}sGKY<;V|$GYiAaB(4D{qicKb};@%PJ2T)XB7nVmLS&u{{jt3pG
z0TX<vR^z9<y`cMz2_O@e>A)x)HIk>}YI}PDe^Ltbh#G!S2HwC95TI47h+hy8;XjL}
z#uOE6^R!4>xSR$o(!x~hGM3ulNA$y~O#>p2YoZ-8*{gEwl_J+<0Q}mbZLD@G4gt-f
z(7guSNPWa6aE9dY?bSA(opwvF6R-?dk`Di8v>5sbcrZ)?VCmr7X98eRV6h;v(xV1g
z*eGY5UM}GWw8WFY&;sSs12h$Ig<FBLBUTNF$54Nug<f6ZNyDQVHfnD)zyvpJ$SMpu
z0D;^C=tkv=Hw&DJn?ukLHMquGF!>K`0+e*NgdtK(o31G65|P!>t1MVb(u6U1VCH&U
z#|p&_)HpUo+b~a!SKVqrKaWn|zKL5ElUHKAlMW-09)Cg#hz)RzU=`pwJ1Sku2z&S@
zi~|m62M6$JPM_}yuPLyK`Au+^QV;`?50=p@rb(0=6Yh(J%_q<R1SsGVq>8SUUn#wz
zjpme&r+at1Wv@}x(u)(I`-+!0T@JVb9Rneb&(m!daOB-Kmjr}i!5MI!*vq=mjnt6#
z&P)E*=U_A-x*mO`krYAz!=%9uJeV}#dr%Zm{Z#3GBPal<1-VQxZFdt}C*5>ilIEyd
zM8E)QfO<3)m<fKO7qO(fvUcMFIC5ly?{JCGOA}o!o=&-a!=v~+kIUBWr02R=v``5?
zm+U|lqksY+rJ1dy0>F#wubN7Bnx0^^k3uYh0|%eubh=Duq-fb4)ls1pBCKik2L<?i
z3#4j;)}S)1H@Z-nTz-ap!%Z|I#Vxyx+iMFkWU}fnGC1#z)DlS|pK^G1oig25g|I>H
zK{fh}<8v9Jq-#M9BYR-;O#(*t8Q3rCk5<7If(5nrNvf;^1XMnZvc&PM8WmIB0Ey=G
zy~AH7F^b&9^-)WQD{5v+=aRd`X|Yi=LV-=BzK{NU78Q&sIJ!-rGzxwquV`GoJFb&n
z9lCESDS-yS+&_7uNjz#yLkE%o@Y0MGF_%%bP&K-BP;BIb4nyX&XJc!SOH%Toj;z8&
z;^Kh$qnh@*3Fir-?x^?X`p_%_XNQ?tL1RzF7Q#xpIX)!Qffo~5AB{qg8eJE9=!+wj
zWjw^$Nl7AKV$N#RB?ZiBzd~|o=tT7ljUu7cfFnjO(KFI}AIuQjlsFs|RbqO=vS?_I
zuMJX~7CRi2c%nL~njVS*b)%u>U*CRj!fT)RrRd;sOc35)PRlNV6O|sh0ffcvU_e!V
zxfV1?bTt{2Z=+WnA`uDGd!yAeVWr;`N@QS&m;gvPXyS!=PfCFSe`!*5nj_)>FuTA~
zT8pM#QZG5nxS^^RFx$!fOkXp&0Z37E6>DNK5%-W=u7;j}JSLpz*8voArYmWRVieTT
zbY6EmE0u1Ta+DP5{Y<KNUwBa@w(*!@TCBT*G0gSP6u;<>g@&6|Qz1lDCKDA2(jo19
zK18RQW$T;*P5?sD5u#nN#PpRI-JFqXzIe0HUi79|6jcw`(53=swqy-Ifh*lS2cD89
z5C~!f5=|Z*{<N~iAlG4=-zK>(k?2iXni8#oZ_1ulvivdF5=Oc+G~jZ8U{04g^_C!d
z@?j#HwzTv_F|qmq$_I#KNcmBa__@;1rXVv>qSnt-D9BYO5T~99ZX^Ib@>u$eufqcC
zNTSIYrMim-GCAeaYoz20oruKQL)~DmB2qaqd!_4G?H<TlpYgeIu!lP1`J|&Ek6_bg
zLBlNIC`QtOOv;{?!Hb}BP5avZ4bKZ_Yx*VzU_B6ctOG}PaSHj3mO%;wE(<=qfGOj-
zLW;zB4#rl*%W|a>Hs!N!PEXPj`K*Z(2L25tP)8Y6d^K<*5ZED9VLYg&?%-0n$D^Fl
z^CV+|teQGe2wOM&5D6GAK@4a~KBH(~^uM?NdB-og{OlaKVEw|A4MZ-8Ei#GB?DYrh
zr_UV@0kuq`Z~~<T+H2|$du<0K;$%{hQt(l8`!F|XxunSsEj}<aZ?=;(3XF2v@yBFG
zbKFPCdpSg_w4CJKq-H;E%HyUUU(MQZL%MUdm+nWPFtPTho2Z%|WMhOCO>!OWLz@Am
zEdQ0Qp@r!WXn%y6?MbGV@p=Je(uAF{0$`AbacMpzeTy0q3xMKwVP`dStu05EQL;u1
z>LUCEdN;JPfFnG!q&sWQ&w1;1mGt^`Y<r8M&~h-~fw2;W@VVRe?#wlPeF8cP-x|cq
zMz~{N>Ox3McxySyK?()!-}*=xtxEME7O|Z8rA@nFt4GkcDt=8jX@=8a?Dlq6uDPKb
zKQf%U#Y9i&&gr|y6wZ0~Dx^O;iUTr}Gf*ePzbIkM>6>e5gmj^7HOAOI9j~#Q_|k8B
zU{O#_8U@|}VWvDEb{j(=-DG}IN{c?A-qFd#X!>uO<$^SQOh}fLH5iG{anlks3<ZWW
zi|`WK>o#NrOsTJ@5GQFWJgpQ1a-eFrO)5qA85X+22TPxprXBU2It`3C4f-q!0SQ*J
zUG>G9fIh;gV5mQNFTc;b&36wDWTCbYxlxKuIzna`*XRMSs@agWUhw_kHpSv6Tw4UK
z#OdhYMjoTsx<&$kB1vO8Fx#Yco-n0*tYl5GI)eE)GI(|KXzTj|$i-O@0+=uy97Tm)
zltiiMbWN&I8ktzPDouM4xRhRj-!OeH>xMLZ7SnmG3=+UsZ=AXDRko^b-fcEZm%Vu=
z36%r++`6TOP&=c_9CL0lb4E<QFB=v_7Ch0SXevVmpg@$)#?or$hxXFcS9id4pRk75
zlSlc(kbuFb9nD~VI0AJW8`TT7<|tgo#}`Tphi)1uy?GOWj^Ak5Q`V=u5WeUpXz4*f
zNuQ~%AdSM3PV|dT4|LYlEku1I3O$05tVfg08&{}Mcg-k&1O^MDC82h_K52^@@sF7`
z+SACMOaG{Gl+TuAqsXuFYpB5yiSl)+q)0pAsWsj{O_QTzkGbjkPDw`m7mi+o8nlJ7
zcDn900D%;rN^ih2PJoPX#Ao3QB0ffl9cc!TMBhQ7*dn+&<l9mi=y@~`-AJ9;*H>h^
zG&&#|=w9!p$$84HW1frvpIA6K8$?7VHkNebV!YuD#6>(e5Qy)W;IGGFplktj#6v0%
zZq6DMbv9Q3>9Yk-vHhTc2mt@&005*1`nk;vi)1QFgJo*rP4`hq>*;E*FV@M*L(q_H
z(fvlOZitDG>t?;aAOpBTyBIY`3)XBbUsSfj2Nq~3n2|$BGl)*(a(?)&Dsb{sFepwe
z8knh}ujS+#DP~3eeR~GJ(YI%E@8}Gi16np|)}UG2q8*@`qwiG&Jj)ApEr@n~(Slw>
zKkxLp`PC*^sXOr4bLwd8{=3imiqL~$SxE3q61QvIcKZD7%OmdJ|JF%GQGk?GRGfqY
zJw|&L*TFf|9n^?UNxJ2R>t6uF=gSxGo8<L<B}m^Y|ND>bpB?b8KK^y0zfSZYKM|^Z
zxc>za%m*8wog5Yb00D$)LqkwWLqi~Na&Km7Y-Iodc$|HaJxIeq9K~N#r9~<V78G&F
zP@OD@is+_QC_;r$E41oha{YuR4M~cNqu^R_@ME#+;Nq;SgR3A2et<YTI4QbFiT_Iq
zEn+-4?#H`(kGpq(P%ksp>=*-7%`%eln3&G2ih)-M=tnPR5R{pz&q-1Op5yBtKEB>X
zd6xINKSy^qXEMMi63;T-u!uK^r#3B}^FDEy73D1PIq|4L7bJe<y5jO1=Yqom&kP%>
z<UDbhSS+-$(#EW4sKk@Rk*umwzL0h~%Xy2lTCTF@J^2emIc;T`>okWD$0C*>L4<-T
zN+`oZlva%t6G_^SdH4q%zeFyDTqQ7aET94vvf~H;gWuhn`H68iDHH=bUu^qh7zpeF
zjhbzLAKP~01n@rtS6a(osRPrWq}N(n<Ot~51}?5!nz9F6?f?T%x@^df<fkd*^T7KV
zeNzVLy9K(|+}@h|IDG(8)K%gJI5-4G3Y5L>@$O)IZ~vZY_V)vQNph=3<Nuog000Sa
zNLh0L01FcU01FcV0GgZ_00007bV*G`2j~J95IPo2md8;500kOJL_t(o!|j-BOjBnV
z$NxR4RxTwF1kzGtD{+FsL@h3$8D0`ESppv@PDn;#vdlo79~Lr_d6ziUWT;=s5@(kX
z%K{6^ilT{;lu)(;GT<nh&`ZDywuj3prC0jw!>cD|$-ZpHxaB-w&htKf-}m|TeJ<ys
zAR>H7!r=oG8At|_fn*>VNCuLD{_j9ab`b<29*+Y^Pfu5=RMO9ji;LlK7=T)>PEJmK
zA4EjNTr#oQZ1hG#p-^#gF|+sf_R@P14uH?+lYnMtXC-}LU|?Wifay6oIqxrrUcP)O
zji;xlnX<C7($dl**|W2=|Nf8@arfiD8dqXSto%cusi`SxEWwSAj#?}hrWY0#W@Kcr
zlxl5lEiW(EYPHqX)pzdPA)-V;h}`=;?t39^k_ZPQzkVB=9A+C{y?Rxr(;YZ)z-qO!
zNBE_srHdCYS}c}l&z{xR)@n4GbLY;9qWC(8E?>S3;MA#80Cw!y!D3loUk~8Ql`8;F
zpFT}Qfk2?Vyd1!pGiREbnwanL@$smC*85FKaP)V!FcFXYe{7m>E{?j!CMG8G^752Q
z<&h&t*!;e}J|g0I9)M1#OG!zQ{ziLyI}w41%F4<BJbwK8*u&xQ<m4oP3l}bQbaVh{
zZEYo@?(S{?w{G2HjvE>p05}|u$g5e`mt{U{C$qn@cDasM&V2usxG=wG&z{oKQm@xb
zM1zBa08XAfNklz8Jpgv^-aR!nl}IEmUAhF|#*G_9#07)FuC6Wsd3ky3*RKb#u&~hC
z*$LqA;loa+69BUxI&_GLs;a7DvDnk6Ppwuf0F6eYP_7QXPzb+`0a$W-X3al~HkZ2M
z>%M;bqeqXbtE*YyIF18w|NeadZnqmiLqmg3r&A~t27`fh762S8vAuius?}<f$pm0z
zWTd{n{=|tB1qB8D{r$`<E5??VmWqlBl}fdF^Jbps6$(XGR@SPF^r%uT_Bs-gsQZVX
z;^CDwm8N99;lYCk0P5=Mv|6oJtKGhRJ4;6ZV`F0gHg4R=Vlp!`17O{{bpXIzHa0dA
z(e>-srHBp<4F!Wiri-E|2tr9oiBhR-Z*PC{<jK&`5P+JR8WyHMzPt9Y@Kfi#hkaGY
zZTe3FlP>A~v!|zL@QpYe4wuUnkH?del30nb<wuVmWyeNDoX6t<uxZmKmSt?0nwpxe
zTerS_cy_zpZnvAwW|PTOP*5;8HwU1osEEy9mH$!Ppm#qUUQQ@e>OM=|pa;MbcH6dX
zMx)VaG^VAciK3XDo$dGgL!r>FUAxlK(f~xGQM1`Bbrx=7VnWIityaq%)YjH=9LHL~
zzJ2?o<|zom{QNx6^JmYV1)$gKU$@PVje*<^{s2b?L*el+nDPAi^8jw%yg58PeEat8
z($dnqckilHDi)yq`}ebHKA(?>Dl045v0>-Vod7%@4-t)zkF(^BMx#W;`Vgx|o6ROg
zv#6-(;K74Z4HHo;8f$B`{Bo_s!@H!L!1VOAbmswZ?AS3DBUTM<ZEb8CtG?#uW;Rqj
zfBu}67{_t3Sj_2ku3ft}Gc%L13xbfFo6GW_=Xtl=otc@rWy_XODCGD1bvj*YYAW;h
z!a1YS=Wv|j?Y0{X21iClmY0_`8clwFzEY_K;P?CAUMq^C*Xzy7%3@9x@8s(T5zPsU
zDalD|l2!k5t9Jtwi9}VZ)zL_7wQAJ|0VF@d%0M!Z3?u`|Kr)aF^xp;g6EY+&W5Q}f
QCIA2c07*qoM6N<$g7bp=rvLx|

literal 0
HcmV?d00001

diff --git a/src/main/resources/resource/RemoteSpeech.png b/src/main/resources/resource/RemoteSpeech.png
new file mode 100644
index 0000000000000000000000000000000000000000..450cd2900b1e96edd4b4863494297d2d9138d06e
GIT binary patch
literal 3858
zcmV+t5AE=YP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ00007bV*G`2jK_>
z4<;vj>Zk?)000SaNLh0L01m?d01m?e$8V@)00004XF*Lt006O%3;baP000i5Nkl<Z
zc-pO*2Ut{B7RL_^Gs6s1nM$330W=1aWz|HnAe!B^?P@IR8jXq3jYgxZvF>JLK?qpL
zViGlKEHqI>Bp4zJs34tz0mOzSYDBS7EkvX2f8K+a5p<*3-0yzxJ9FPHzjw~L_nrgP
z`kR)#v!`QIQ=+$SP1>1|fAV;Tw|84xlm9$vm%aO8N!L9>uXXyD6Fv!Tc(M=uCLFot
zWt&F79ZF6INLfx=3+c*8TSAHqQe9c~71u4t&R9yWWqe;V-v`ffo~eTL@gyzE%m2-Q
zPFl)i3_bHmY9aS>o=+*?rvz2tYpoD?rb1F5A&=-kj@wT<Ga1@m+0-(gDU<kBx%vMu
zP)loTR{u8(NL)$=v-P8WOKmFKQo?6e<$0Hpr<rGZ0;Rnt1hbXN=}zn8Uk`NsyTdso
zH<KGQZ{hnD3Ve<Vy$(7WzH{_UbM!di=$Ywg%oCW(_<p4vmJJGq6bMY{hW^C@AD|bL
zTj`*`{Ys$f(bEf|7@p5zp;wB?J>E$l<D`pq(s!tA<F_J@V!n@sAwy*m_lex2+lN2^
zI+$`v`0#$q3j<j+Jqk$kjmRDA#f$Zz*(T9#m*{tiT@u7DJH;+>PSz(@+G+i%k7JhH
zD{{+lG-fy&Fff)GVtwmmt?^8UVu5cF=`V$^e*vI}^-Tq&OC>GFV<pTOCD*Q0sza6P
zl}z0%)g_8uwo7z?!;-tjE-_X;Sy|)S)dm6FTO_)Kb|97sKg-Bi6*T^NftpX8O(9{l
zK!q8x>Q_=72*Q-=PnGI$r3Mv32*b2ZVqF-lEGQbIWZGTa@(kP-sSOaLPSkXPM}@A}
za{*QE|86tUR<Q=N;JZJQYgXEzaP?EACX`WFs0}NG^)f9+?P3gykBO99=Sgs~lEpKP
zYvmfejTUQ*#4r6a(7_Fxz9NE+b};&nP*~UKfbv9Ds3T;~Yvigm5=FR38YYo%<jFSi
zWS<LUadz@Ar0Or^nq*j3tVacCxI(j9p@9%D((TgtJrn5BW9zA~dC9-F5G63*2d#!Y
zO))G)xRX3gAPyIZSMo&*T|LGK#p7Q2-SS~0KJxNe_twPE#*GjC)6lS)GvARa=80v?
zMe2=WZL~!HDWl6(4A4%#ddKU*9f2M`eAv*?aP8W)`>m~?jGYiic6*rZvr(q)Y^DgM
zX0==uE_7N#^wG=RS55!mqtRnF#l&P47KTSfC8egGx^MyW+<W{8r)X_M{pRi4mxQe7
z+Nbv)^lobvs!(P^>zoQTfD%Z$`{QLl12s1{U%h(uhaY~pR$GhlZw2-ZA$#@<p739Z
zB8(>tA^u8oSZXi$o1MebzmCtfSnjp9-fn7Yx_7Uxz8;@+;J^XQdHeS5TkWN>vGKx%
z3*q76-Clh)*wYv)S4Fh%1^^|AT|$Y@*4?rK)z{a9<I0sQH8nN2>*{XYx$}m*X9UlY
z?agUr6`aFfxTH(BsECNfuea_?+B<Xc;<}cWru+Brx3pmR)%H?bTYK{4Nz4q6n>TOb
z39^e9FM{dNp+lywU4p%RX2?`)869K(NQGu6In1B_0frDDSQJcjucf8nL{%Sq-e)4I
zt$(ahYQ9uzmN{#d&YlyUoP6T+>H5Y->tLd?va-55P_+R8<;IO0KmGL6%$YOM;lYCk
zkXZ|yJ$v^2`SV#>S-vm5G}z=jRHj;_P%ng@WX}I28b4$h+l^S18#iylhnIzg{ekFn
zne%Gs1NaI}xKz1L>O5Pj^mgErKrm#)h`hYK4?p~H<j9c@4i3Z=I2<Md)8ogFA;T`{
zfcEj@$LHtgPo6wktyXvG(uM7;(&+*Q^dIcvzC^CdQfg9U%20R1_N<J{S8LjUK-2i(
zK}luBNH5b|qDZA?zTA1TRQ?aS@?)7Yvgd0<NB)JLQS9yQS;5cGZ`iP5-Me@1{E90h
zMvO3+Oh%%y&e~~m)nTPNMX8B*(k1g$gJmktm8tH>A9wzJ`aGgFVtJ%Qvr42{FVe<{
z^k0i~;e5qcufKu0^xe94@%QIeva_>eWjm(WuG+Z;WyXvd^XJd6s;VlftQ;kl4U?<3
zC^czHHLjpTLSrS*?^{wVTDG(es3PT?|5#5N>ltOOPA<Eg+K$moN5e@wQx$pT4jh*e
z9Y25dr}XT?SrDaX&z^wTxN&1hNXV8gTP|O|eDvtiqN1XVoSacA-4MBIk5U~YQzwhu
zEe^hBf!}#;pDCg?B~j`tf$@Abt~ai-w;fKxa=stJ7qh?@Ay_%7bALZ{--PJ{Nw_v7
zG&?IR^<erTbJ6MzUvAyD?a`x0@L%}Q-Me>VV`Jgwr%#`TN=}_Rb@=dMi^YOxOiaw7
z?Ch~F9s?xu^-^V$*cCBJKDpoO@m874e<M*H3lwdyh~ukp^u;q;ff`YFIO+C_JWBaL
zhg8`@FEbg-$@8+jM=@#5q$nmuB8fgRbWb{(S#_@3Tv}RKSXf<M9UB*S@@#c!MR{4d
z1$*$|!Gi$G$;sKfcW=VJ<louxJ{PI?i`)^}f##C$u*QDV3du8vq*a%`w*uX-Z-^GC
zi%47P;0I(>geJ!X-iL)oE(V2wSj-yNIhabM{+9yAHuQP#n*Zo4rlB>eK8d6apFDNn
z(ah3I=PfmtHhjJ1NWz{2yZ0m?N;iLhF00IZ{POpwuGOsAw0=xr{}j>|3cO40y$kFN
zHQh!v2hBKV7+h%Yl~3~8s#C38#F##9Rx0sM*qO|XKDdY&aES5p1ttWVY<s4_P(+4v
zo`aeP%xLU8ts!vg-N0!-2E2XFB{*B`pH1o#k{6S+nN&sO9803ld9Ld{1GekC$B=U=
zxk1gvJnw3^0k>a&=lW}t8Uv>m+8Ys4=98ki;$*9B2%Bp$zwf9iB25)shu>x?M7{|2
zO1aRO@0%m^#uKxY2>fbZezU3n`}Mu2)V=;rL+^K+`n=cN@9(v*{`IW;z;f9uP-ih>
zd-5nHcMH$ww6e!}!(hC<-De6oP|v(F^0+*pi06&9@=4Zs`ATbtnEJ=o);SC2FC;oo
zh|jIdm-D-9chcgX&k`Ep<IqqMqbf{M%r_N~r$yp_)qgawpely!T|685PDRU|pbzc_
z&#LSFE`(csr`!$t;AZd1&Hdg#_44pL{ioNxHrXuoKgKh#-<eCI2dq=u0^w4_IbU?{
zOpsiCkd!AJtj4m&&P>F<TkMLT7EXUwCLgE)SdgpTgFtq>*W2}drrhc^3I1`$wZGZ1
zOFnrNlA%!gQngFJD);^uz5jUHt$((}4~~qnh0N!{hy`S5?F@w2<NEdMxb~ZWdic(J
zQ<f9$6=<;OxTFHR4^esvlRVfaHy|tGdBINsmrtH}7T6il0xeq8MQl=m7go)7FcjHa
zg$#kGx!o(Q2^x8%4;k|mAQT)SxSye_hC9t0lXgd`x}G98C?->AOl2}2Fu{o5I(nEp
zVLh|LB0_KU&l8$hf^5bS*|+e7eIz}+?#rhG;c|wOzyvac4Jie=>qoy`O3q~tNRzDz
z9^jDOL1)_}TAWqbePxVq{cj*_4(nFno7kV6BVt0Nm+Ocw*4I592nFF48^nMKaZFlX
z&d#+_*nMU@FD?bfL`Q^Ph|jl5wAhCDbj$z5DrWbA?N5FiO&s6(WAAQ1U#?FLs97bk
zffdYmFef`{?_M|{jLm(txg86UKa7|pcFPmAWBrmH*|Di2538n$w<|gM7l7{FyN5W7
zi$3Ay#|jH~XB|4~6<9_(o4;@PvYrInfeG%FD0amqW3f56I-9SIN$eAttRclgJznc>
zyYw@&4lerSn2w(~u{-z3{t44dNoltC?yv^b!=4W8F#$g-U=&u?p4D@SJ?NvEKKn%a
z_lb^Qt9dRUT;QDH*aF`|G`IiQ(cNEfJL2eTA$^I1PX{Y}EAoJz;@ba;faE_#<eu*6
ziNqf6lEVB&fJyuHsUnxzL|gVGc3Af*K!}c60kK3yMa3<f0+&LdUQ%w^m7SF`b#5VP
zaXpndm~2&rZHV<^+k=@no4<!Jsj41%Rhol(IMKTJ*v_jzW!sM$HHwpm4<Bx;Brc&r
z?0hIcH!&kUbJDC_aw{j7a`LLM0{OCkF-mi6vkQ~GTcfKaCzlkl{u_Jee-QOBdYjKy
zKh-iFffg@btkdaiWq5aICa4Ntb>ZUo2aaVY9Ld<a;9p;udaWgK66tct?Ian`lko(3
zrrE1g?49@6DgQ&FX+*t<-UuGF^FSKAZ+;mF5tc|Ka&&ZDzI^$-dGlC#>C&a4p`pmL
z5)u;7!4?Y#6|hoLQYK8Axbm~L%OcnG9uyo9)IVtKo4tn*2^=`k)62`nXb2qor+-JT
ziQ2sBqXi2xGBfYoxdZQfmhV0R<l*7*)U*LuoNP22ZJCmjli8>q{Smx&>(&kOoH=uj
zS5=K0Hx6>Ye*JJoB9Y~~;RE2E&jqw($r71NCKL+i&Ye4O;6PTMJ$p8f$HQC$1`L1&
zH5!dA5E3Acb>hT{XU?2KAIO6S4Z;&&h@7}{x7KHujy|MdN(Z@b-@cegAP~Tmm6eqo
z4Nd{u#KX<a4UMeb@lzs^Ae!#n4U{hc1g&sDoD8c&PplEgj2QzTPfScqOG`t3!#0i`
zI~I8lyp06{3K}t-1>(A4EUXz35%B^*QmGUpvA?3CqBwcTkRi|wYXz7LR)8s4efTp8
zq{(Djv}jRSSQzpiS65fG1P2GR4hI^po3F1g=E2M_0JM4YW;W$>c6R1u+p=N92JBb>
zA!CQCpeLA;eWg??0|Ejdz!w2AVZsE+K|w*awYB^A@8`OyRH_#MLOFNt9KItxJ)M(b
zpxoTtq@*M)!}h`3%*;&oZh<^&)+}gf$BrF%Mn*=m9)`CoR;*aKaAAIaJ~kRmxnRKp
z%!WB%4Cwg_QdOMVSeQLLI}|js0r3mX`P=+AsHLT)si}z#GT8$Yp!e_J{}ogH7nVN6
Uw};7xPyhe`07*qoM6N<$f^@on#{d8T

literal 0
HcmV?d00001

diff --git a/src/main/resources/resource/RoboClaw.png b/src/main/resources/resource/RoboClaw.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7dbee1a55f46d2c861804cced61298d623a4b9c
GIT binary patch
literal 4405
zcmV-55z6j~P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf5XwnJK~!i%)tLuW
zR@a(_olJ5kncU=R;<Y3iqsG`v)MzYNf+(PXs7UWf@4ZQvE@GpIMg$d65D`SNcd-{N
z5l|6O2oXgog6)~j;b&<kajvo4m3h}<{b%p<?egyZ)k8b}pW~eqcqaR;Z>W<!y<PlU
zZeUa98aAe-^6=?19zSWL^dkPK@73^52{b;eCm~@mbJS*X;OH^xAKs(*>P6YxHneR1
zFGk=&-972|a?w@h`-kwLsga7?H<+!eNP2oYPsO{t_i7}WH^n<4@N?lQYO9N7E!eYv
zC(e!z;x<-D?Z^l}P8A%bNgVv+Cr_ll*Tq{V(5ebm<)tjJ*QM<05vl!1=>3Va=fvB`
zER0$pZbF5dw<;*FD53h7N=fE*@lFWbzHx&P2V>k#W^?+$Myam_8C$aOo9|CuV?Ebz
z{EXrxIV!8Fq<1wnGL?Gv=j`FFt57pfI972CKL=Bsj5V<}Q)A7>Ao1o=tAN1P+zcFS
z><JF?m)g-nrOI!XQg!zZj~@Rym3lh_WQW$W#oXdNeAha1x#2uvu14(5OJMuH6~txv
z@bKYnN$y%{8JSsIr03}9aMTqQIe+mScklm7Y3Vho|5fpZ1fE}aHMN*0=y9?69A_V#
zU~#@L`*&^S+}>4e$&Mvttt+u>9VowcST+I{FNww4%8uhFkJ0e(J~NdSs1&yGOKlDJ
z?mv)8{oi;40?#+Ur7(kCRXJR5DrfigOq^G0;pOj0$?axJiY~BvWgr@1x>&~=6X31I
z&g^Bfkx*V$$(2iIu`sv9!pM-yJCz(dn9tQqh0+QwF1Hmet><gD;f-#frui;s8qSb=
zKAU}4vsqOTjl)VE9Kto(9Un#f@&xuAzsTMCCj_TNG1*;(@Gxu4ji-^k+(XRJEX}iX
z&mKHHy~OsOuy<cRN)zQcfBp;)9zNhkX{jXBinmDM0T0<(vV)ZDXpYw(A}q%l+f~|V
zhpFP=q)z#^)f`EVAuiOPHR(B23yru~bP`i9J**tG2y)iJ*I_QTH8*54Lh|Y)Vcd2E
z_<9K{j|dC$;n?9rG(B#juHm6f>bH0af!|-(vlrThwT0L&p3QB_I8?D6_Y@P{*O_7x
zKZlWeL+RGLBhx1g=E><Cit|#~v^0VhN$Xf06DT~BBAOQJ*jsDhVy%ypi5kU)`=ojA
z)z+~lb&d2K6%~f^BzcY;K0wo>22O~;_eXf?4Yau9rluxYs};?HL}0c%r|SwRuD!_0
zgQ3_a&Bbq%H5Q55O!1h&WP|VdvR6mixBojvs^fTeVF%R*GelrBW6bycFq$(SZ&zLB
z>ZqbJa|$N<>bRQC;^?ka*(#8ll8lX|HSx<9iPlr}v}cnmyjEj#1GRN88uzo`6DTb{
zPx8_bS?jkXwv?ukye*0IcMsv0Zbj<ubOKU5&|5T}dCN6Xv75-Cnf>Y9zazc6ccSBG
zpE7CWw=@e9DieoOdnq4x8#NXN7!wiVf{m3f8gr(J4XWbjXuz(Gv0|0S(pu-wo#o!W
zYFzB?(AUspM_xAdjrU1dwpe=KhTjm#NsA}YMi)=hxs+TvFY6U2-`r|!(MNsQRJ@XT
z%VyydY|i?$os`u)!f(X_)B+}<XfcjX13p1VeH_zfjHN@z_H^y^8GXL&M2Mq4htguO
zG*iXVaW;;&v&mZOO0bI-8amT4x0r*6trn@VE;Kc?M(1nSZs6tYOxpT2LX8?xR#0GB
zOpLgb#}pM^k!0FE6FGWic@eRG_9Fb75aa7iLtPE|nX8HLu%!86Yaw!{z8a5wJHik7
zpy4%{2Zfutv_FMi+cr{C4Qcs#d^)N<&ISrh9?+Thdv%~|@6L1+1iE$Zgzk)qtPHfI
zuKbcT-?}&t+^kiI7JVLGrug_<5$bKuhIk+Lr-gI3qEM1+!L@6}SkE)V(#V(%>yoLj
zyUXNJ!=-oao-{Y%VmOmX4}13HWYPHGS7Ll@gw7hUXudNwRb|xG-jRfhsxRTb!vVjA
zYVb=IO+~rnCq@zx7|gknI~=)ioseKVKJN25_O|NC4ev##FFvPxmyUGp@G<*0CrI<O
z;A%lS?mqfxYs;}JF_2{|Ls%H&#*X!2TochdFV2rkCvs%ckgz<S{O!5ix^<mFy?UTH
zVZ78YsgQ@MrtlD<TsXd$P$yITtqs_^VKq(lH7pi4<7cVEoyrPn!0m=g46KK8G0RlA
zjU7BH+t0bpOQ|@M%Hr7NnCqx8MM;6t@`LIBT`wk&8OZQ{J$UcK5BMp}o}z-yY}*(u
zHsnEKN)TyTORzMV#@6)<*|8&utc_7*C-{&N=f%NIkrbWYENrzkkIGC>Ww6-%*kMD^
zQ&*MxBm&L`vk7&!B*4y?C@)75?u)p5@&J*}rb4X@*^-h-Rar4tFPxGFRaaLqPqROJ
zQ?x~~yH)7delBcHpj(#?#D==kM;P!djVUCCIbx+Tk$$85@M*UWBntw$tNn?Oup(0!
z?6OF2GIy;aci$SEY*g7V$dnygO+oHb4s2P(={-s8-5Ez>tQQ`3`V8;emkA?Bh`;Z!
zG}M#!A`!6HR$;!42=jI(q{S^{%bMjxdDsx{YR-)-7rAliGy&GS_?T%@d84&ZX%@l7
zOm`R?BBlsewS`z;10ww$$XmMzwdwLi1-mh6<~XKmj78T}nbDJm;Ham_>71n`MA@-6
zIhsD>2eL-!eC73noW6gQgyaBn5`8(fdlhF-ZsN?zGzv~{AUMzlRmBO69Wn%kaX&Er
zyCJyQT1)$o2w2Wm!qHFze_K-`TrG+6v?V1rjHhCgnMtt(+ZqsTqsO^phqxxHtpi&#
zq`@tir!$%)pE2YuQDaAD68ka}m}jQN`ut3SqM}44X2;lR!-Rb;MOSq^T|WPSLH#?E
zkm$p<UF$fsGl!EEJ8=v);7EBk>oY>hO$nsvTsGG(<#G1JMuPmTP@N_S3>5^%j}^8x
zlmJgRX?=-+`K)PZO`C+5g)U*v=7cy~uyI)gO9R|l;A$ZZ+?K}swH(gf!~#15yp3mb
ztK?^Cf)==2C=qEnLg;k>KCXsD#3zxRe~{$l4WuV6BWYn6E<QTs3jNMIev-XEm$Cmy
z0hfQiNYGjjmhABtj<K1DE%Qmr31Q!!L~>I6s5rlkXkQBvv9(Z;`<~H*2O&S^d&UeM
zNQA$yw6;WG?vyc%>emZ*Va$FuhWOc<2o<s-!rg`qiA$xdA~D2^a7Sapos7lii#WA!
z7aNwxNRza{UUw#;Hku-<)Z<7|8T#7ltk2rb#*I06y1H@r=m{#SZ&7vcK3A$5$roAb
z*3wPfs4wQ|rCgj=nV{%3p15p3O3Mqla&|L0>*kAG$^jE?B~%o~F>262#);D((EUrU
zTsSAKD-m!q*5U52Rj5rEfswi*p60qNa55p(-A-)Wn+4)VTF9)55EYb&8@X$e2oY}9
zcb+Dtm!JFVjJRkr7K;-syu{{h>A2bnC*bSKxswNF5-kbcO}3XD=VbW-QOvlD`r#CM
z5o*i{Qbadeg>?lnTso7>4<o-OGsTzfn*%V{S7y?v@A$57Z~FD<%FP?sWwQwa&t9lO
zhj#l6jVW?C>dzFeR1bf9Qx<yL5$0yiwzWU;tobR+0zFwMZYRdwlG^HDDA>83CB7}G
zaVrjN-9(I^565qov3P9)zF|HT9?caQLOndG=gh4W%&;59O|DQ-eS}&5KcW#lP2^Hq
z#N>LTU_O?@B9S1v=&?C9i2R*ls7)EpfF9lG`&BpU>%}}TK)Qi8g!;4^#*`5QFq^4>
zo2iy$6A_;F#09t!Xm3ummjg**fs)*&gvA8f>fvcTOZe-PQeO)yORp2_?@vKVF}rX5
z!oj?aBrLEXCexnvS5|PRx`gcWsm$<^V{Vu-Ug;KuWH{0O>;Fd1;0I1r9O2a2EOzBZ
ziYTpyftDt!BDVYyenX%IPai*`>qqb5Hc#a3TGNI5)E6$+h;SDR!rbl2Too_bM7XQS
z+imp;vp3;E^{<>gbdUf`QxS7ogHB^z9SISU95{ZQNFN852Abiw&YT4YT!`7@%dGhd
zqDoU{!8SK+BX#(%?tjC1xgH*g4(vU>kIfr`(Qq73Sh$}w-^=g<fmT&|_Us86Yei1}
zq?vI8dts_R6(`|h1MLi@AQ$Okji0p%-d2V}y<DX+n^&%2zPXl2Nz^#8Ylqa=f(Q5R
zk`|x9;Ql>v(N`lN!;X-B9{6NgqaHMssGVNSvY*QLa|bX+cPNfaj0h8PAul5ab)}&k
z+4nq^co}|o18ulhUBzer*^VV4zUZsSiwso*PfH{GtPJq7G-OF&s~d~<bj4E`v#*&p
zzm}I#eC{HK)0Jts*ZO&}rm`Ac&1rnvvpqrEUGd)Ti1$_-7Vn*pwvRIHK5U2WQca9P
z=3?QlL+TP|ROCgfa;o*C*uTOb5NN%LriYDOy?BB8yVVTu^(DqCQ?S-m$IWOCF7xz-
z?ziMj#&`=-{<db)n4Gm~=qOD_Q*k13AuZp6o+CEO6~}ZlT(Yc)KORJ%F<&u!c0WSW
z98nUtuqf7z1@2lHs86P}s5N7K6}*%{8_Gq%`t1F7q^(?rrtrl!nlrH0n}d&~9$v!d
zuMBI|(QtPU4Ag{PPnBbTuJAg-6!a98xp}2E;2k`=hf&@?;=RotrzAt<bcdtnHVLP2
zGrar^h3lS<lc+;#s#=rA|Gkfw;iW3nhB^^}Ywy-@=k`s8eBA|&AAi6~TMZ{Ybv&(&
zM4>Vd)0wJht4<XJCr~BaXX)i4;f^Pwp(rPkIdy5CHHoYEQKZ<KF?u+zFd#6+3N_p5
zEQ+wje6Ab;&L$$2|82SWD)^TK+Hn2nLOOr+KD)MTKt*mOrn3~K{K?s9Hk!)vXedmS
z<Z{<<V&<eV%#k0@LRq-in?4JbSu<Fho=9r4HxnGE;=jm+)Hru$$`2$;c&QeZeI2|q
zfyVkeQJpnVbGwXwU;Yz?QA4nttBU#TY0OcQXU4>_A`KCzDldof_>pW&PnX8Y4H?Nd
z-MV0EVL{3DtK2-kiRkcoqF}d0d0an|7l*#$S^qI!nLry#t`ze5-`Y{IHy<^TVCgAM
zMCHfvOc*f)9aRMs$Bm|>@S;$on|%94HwN_iT7>^@=oyHBw{AVlmw1vNs?F4K{n`A}
za@nk}k3UVIQH1j5rbeN!_ZZl%6C?YG@^1VnMh^HEt?5&lGI|6{{O0p@m#z#Exy#UR
zd(*f37tFJ<A@102VxkOC7n{%9kSv?=&G4rQw4th^lz;q3JMwcf7%3%L-O*6`kuk#t
zqcmX@{d@Oh;5R+#_e~F>LS3;HX?bkC9lBFTad>;SY?e32YZG|%sG0jB(l$2K@zv-5
zpl7EJs7)Truzue#UG4`4^b)Eg>eqffd(ivKu2>l<<6=9PGlvhzW_T0Le~#BC(1yxe
zWqjDK9s73VGImfOz7_gDZR`lX>G>6X#b2ME-RUWAVBpu?*}G$V>mX_Ge-Q#Ls3>dw
zg8Z%UPMtsfJ5zrU1O$=(y}zQj(EktL69o6}kz}6ZFGQdP4I=yOC~~L7WwFSAKaAeu
z)O&R8%x9l`#I<WBGO4$Zw??1^>r;M`euwnx&Oh6KBGl(Ao;`j3FN$}8w@RQ5T|fQ+
vCHcv+*1N?!ArRo}DSLYNcqat@eSrS~gvRSV-HDkW00000NkvXXu0mjfTO67M

literal 0
HcmV?d00001

diff --git a/src/main/resources/resource/Slack.png b/src/main/resources/resource/Slack.png
new file mode 100644
index 0000000000000000000000000000000000000000..3544ef16f42aec55166b46872681c659522cecac
GIT binary patch
literal 8951
zcmV<TA_(1yP)<h;3K|Lk000e1NJLTq001xm001xu0ssI2*kEqZ000}WdQ@0+Qek%>
zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3=Eaa_BSg#W{eF#-Ex4%Ue0!I(c^;xwBQCF<(F
z7%|8u&kEF%m05ty{@?!`^FRD6-bWKtsk!BB`4?MkzVl19&%btmIvelL`!Bps@%K;1
z&HIGsQ24yE^w;&B<LUE;k9WB7^KnyOXMBA<$h{x@j6iSJ@9W1wa@;@1U3@*L?dO-=
zYWZ=f<Mp8Vy6_I=^&Q{CdOq~G`RPExN-OcA;QbCR*!#0SBMADAq2JHWH-qB$c8?Up
zhYRiXFbwk3^gceV{n0=_8TsQk^VRzA-@X{%_t)|8#g_SHBYyeq375Vc5AWCbFX8YS
zk^lLGbvX9xbY9=*ta{FVX4hlJqWKk3*VyjkF<i()Vz{?uz6#%o-^=}Kd{x`+#O2FQ
zJNTNeIgeM4anUW;-EsSVPB$51^xGFcy`MhZV>J}t{KP*F&++AsFL-Q)ivOnHCIk-q
z_gLJ0Z@cfCuJY#PxYRoy=D6kCFY~99|K`h_Gp~hhecOt0dBruukmvN1t8j=rFTbU0
z;E%7D`|S_$qfm`pCNozi*zI^8F_Qa?t#JJuxGwSeq>wXZy&iyxcz0qjF5m&1T}UoF
z3)mFruw%tRosp9#e&7L@l3xtc#}KkN#aFL)b5C*iTAzIOIt(`9h!k=t_=cRqSurN!
zr^HGO^%PP}DdkjBHL0bZLykG+oJ$tS^%6=fspL{hEv@t#YOJZ|T57GW_U2mv15?Yb
zwAxzh-J8yfI?w7{-#Ie;2qTU(@+hN@Hu@xdW}Io}S!SJW_T^Vtz{JX{th(Cj+nEhg
z?6}j;yX?B#?uS@A;lz_pKIPQYPJest<<(!l{)N}v%WM96O4pUQ*EniQ`FMmAoFw@S
zkNN2Ec##JPXfL1H>SFZroP1^*$QF`kkV(EV%gJN7FmC75ac|#!dhW0MW>WntzxjXi
zoRRDPKRjn}-LHQ8jn|gy+`JxpvQRd)f$TducV7+P*7D=`pZye9hFfD+=T@4s@U*?8
zIVaebU5lhhn<bbu7enYR&VhZbpXGptIqY869#onG@}=fxs&l431!A5lj*yS%NhDmd
zK%ChTgmt|$i-qTBP37<-^H<ty<{Et%7Y=7hxs5U8-xaf~ivjktxQS3TJEWfS=!drz
z*K7{hc9+WZc`cStDz?3~O#p>8m#;f>PaBKDhSVsL)6QkW9-werv1i`abKQ|ifd28!
zZcneLbPNrcj2ZffYXyAXPFl5)O?_G3T1PT&%syNrRLjANEmW80(O5=21X{;Dxw)2t
ztNFC*P73?%l`1y%y|0ww4s)7`W6t(@_(BTU+KGizN^-l(+IH`vds>fn5{0;u$v^Y<
z6gG}01`)bpV7=~64(#kl#tk{omEyvrDKyUucGP{CwKIvlyseetmOx4koLJ?oZSt<v
z%tA~s+JL{Y);uXyPoM=&%L4|OZ)c68C|Iy>-?k_&GI2H!VQGA1W|&lw6POF;412rH
zePkLyQk4~hXu|nxo}MQ8oFI&A4l>q}v~Fc*?yn+V|LzlPs5sc2)c`^qWmc|dN4%7B
z81j9IWBYJUvxO*U&}lJfAIOSS#=xn$M^0@bJ0Is)Ve()$Ni~Kg+HZQ1nd9_0sX4?H
z;Y_|%{K!RpF0e)h9?rEhHs;oZ3HaJ(G<%)B&q)qI$d&BN`8?wwbv$X3cuv4Fr~BD@
z-m~-E*m}Ef%jv#tT2f@!JZb<NWA%7ll|0D)gzQ$hmzC${AsigUy6jMcj%8w#V-4Hn
zY{o1NAc~s+0W<nh-Mq8J5WA*pT>!;yyGkA#uqMih#Hd`rKtPoxSxqG*6jpm<<_Rms
z!bJC(jl?fKG<*Ocv;byam>Q9()enolKh4(z-+a!x%3f=iVS#^`X=O-4RQw&`>P)T3
zFL;CSCee=HWJ%k>_9-5&4BEM*HUi!oAc||_kOTt0xyb2?IdYn&PB3{cVhZpil?N~J
z*D`7Mc0&FLL7oM%!`o-!=qDZ=SrNEdDS{^)p75iEKVJC0ZZ0mFz6|VECefRLj&=+z
zG-7$e<_TZ7N79Y%)OO=(avvpiZC(ET)#m5RiI-6TLXf>CuzT+7spd}D?{rHmothbJ
zbbt&Yka4mB?0x&~sS3m1XO2N><MkuGy+3~AT=RF{_f159g?>Z9jj>$DnYH>ZPXaaO
z_;`-W4=*loi$FY1GG?F752jHd3Y525NGkpU0aFAa9jTCKC-$TK9Sq4%`pHg^e1b~9
z1>jVlgo0k;^gGkUn`fT*<6ANqGRVk!j86qFkf|?}9!vuvsAW&C4Xus5Y{uTrQWl#<
z1=elloxV1pLZq+07ImgK9yHr&#JXoj#C<7LQcK=a;;9q_3H*mbwh-gft}w$GDu@g8
zAjaO9U|ygwIC|VCDsc7JOTW9a?ljsoROm9jE-MdYh{k-<5o*Q+o{qQcK5@L#Abve#
zUZ;SJveTm8oaUJLO*WDu)o;rHq+k@851-p<BIS5{hDEL=V#1;CeH>Y(i-H?gqB!}T
zps#i8Z8xPYp0NrK>yDZY64v7i*<Z8Uq2#n1>`u%jmQA7FpaoI`wt}CWrOe{{a1&}X
zYMq!n0DS1V+Oq*t`miY~or)*Zxo3l7<amT+L$-hOh^YLJA>U(RZlcd9>^RP?V!DCw
zT*#rrG+{%Z&$B;0@z>`Cb07EPv&37vN@!oT?TuY1MtT(VI4}HD+Pnp;%u62>;@%BI
zrnODPoJEGeNE!q~10!}wr+7-Dyij9q8vQ(>_k5iBZrG4i+>D1dwfiW<3;dAi<2}?s
zSUGVyz&dV(+yiVt5jBudAX4Y(UXTya$D6hgAGk|ZUiZ<m5*&sODHcDhVyH@Cm}D}M
z#GwvQA)&szAZ1A0J)rfFHQ=XH6-xqZfWJ(g1WZw%ohsOzujqmi!fxFeBny=h74k<!
z{%8C)-(TJ$@QwIb)M;HRczi1Pc~^4OY}s&=(~QmYxn2De*vIUjC%>E%b*F$T`zJ6b
zhS2&XtKi7TAbc?MSUMAA7775#(@2(>RA)gY2#+P77(6f-juK391>xJ>ZGs3t7**Gf
zs^c%HL?Z<^DX&oxKuEvlqlly%fI}DQm)b(t;b3GxOauy|kfhRDg=A1nDq8!ETC_@-
zR5_H!$wWEj6)a}FA7`hR0+<~joynmP4M7(`DOB1`EU5fgKt&<!2^KX1C)fqp5>e<+
zI0?>1?SS5n_#~?1)fuK=M$9bz@r&x!sAV2BM~Q@`>lWm$kiRoz>02}-xPfvYTSOLn
zM+2W=&5Pt%xO`>`Ll(Iz;z}RFKB8)?$SOJ7kaMsX;$=M%Ahdq?iuoH(P2EZehkn|E
z4v8zq;vW8rIIBJ(kBsE=!ueE#j{OKmifKd#;2<&*nl&&@2_D}foi5@y(PCsU#G;?T
zUvMIm@Un|2kP0`VuHF((B@Tc9f&)ExW99%SM^%LyiX@_Ze!1HG>2jz&M(8<1=-^cj
zd8o;HlKai?A!Os=Swpl#=%3qFi}*+%Cx1PM)TS(#qY?%zC@qmBaQR`9D7v~1KEh8V
zKOvaJ6{-o8XoL%w`%|Y!tl-)-3E*UJ_(q?5Z>XoS8bpg^plmER_v2AtpxdA69T(V!
zn1QedG!L~<Y#l}nQ|>C+X+$`W8F^0BB><~}OO-8^a>?V-faxR;`wbV_>ddew(A;(M
zY(rldYpi=h>_|H08ufHv)Grjzg86nUcA-N_ih%CTb#Mv*eWZm7>G`E;VZP~RAVgbo
zt@+(5Y;D1ZUr`khVc1v`Q3mkTA~9kdQJbMyBsbuG;DBiIE)=daMy41^0+5m^A4&+)
z3@UO!seVH_VA2hRS|+LGzB>ODCxlc`l1*?I{uW%u<NYc<tR)4Q;i05VgGpYM;V73t
zUN!(V2J)dp){giN4-}jbjsaHEp?|qBDlC%+2%8>5HPro^^FryonX>MPumvEh#NLYP
z<G2zWKB$<x&u|O+1X1xWtACUMW8kQxKH=unL9;wa7$}X-<~v_J2&RLGIRMUfshfbw
zT4%W68QH}AC!CCOfIoR@RY%>}*5Qi;5vKu3TQ+s#sEQNC5;;_@UDcQnrz7h)u!XSK
zy*=WZ`>VMwU*a;WfhCm6q~)XH1tOTUf=En1ONv`k0c6hYwhpNnRAzO5cOB87XA5R)
z0W=)ye$_61FZ#+f{PJi55+reNx$PhcJN(~H2Hrb~3jD&TscSffbifQ$RM>nElX70+
zl;o6HNh5+#xljgZ6_&zw@M3@n<$(I}kVTZL86=7?Vg}+lDMpCEkT^n^2$rTFOFaZ>
z64G_Y1-SubT%cSGNA+J@&ouDO%*2ii-mc>2bYA07onKGl{WFbxq`9!|WmFVf3Qg3h
z+)75T;SF8KxS0$Q3AchMaFaqIqep*vO8?!fItUqc0t?^ah98IfZ$@i=94^`rcKjag
zzZ$H0Kje4g{W9EIRe6vt1Z6#>!!06&L`ug{G(k)tL+_=W+{TGhZ)dk=V_pzdVhQYy
zuUC%Xa3!)r#aziBQeBaxjqOrMbUmafR3!{~Vk@yg5hM$g0VjwbY-kunk}BXRpRV?T
z`axA&NCZ&`&2rQ<<RJ0`LPoKHB`v6`-xUIJ^-uFoxJ|e{lPNt$)b^a!rP_d6CtQj^
z1O#rY`^UtqryNxAci$=X%rd1shi|hjh&vfVJz2hkIKv9=NC6YuYv%Csn)3n0z-hmR
zIxK4XToFuupmdiHdBWi`K)#bW4~(mZctAIU=Brb@u|3x86g28q@U51G2&oNb193xd
z#<sfT9pz_;Wm1tG7U&W%$xtjRVHaTP&`;^37kV*;<T3?VbRUHeMr$F0*en^!2B6@7
z2hS>n3OEcgjBIF_feO5->~~xT(9Uoj&=UqZW&l`&0&q1#O?ohwS6si2nAf54pmdEH
z&5x8Kf-gB$@6TQ-CE6?6+OCp{*Au0sru%aoY9CUokjg<TAwW5G?Y0P7_X^ccg}`AE
zM;3RrJGK{E?$jh`)RcjJRdYdT0pm+;&IYAma%i!3y-ztCfEntX@lWN62aT8YNFdY}
z56iJqdg?Tyud@X8NANw#g4==?<j{H)*rYzQ)tXBqEual>3UZG}5c#gDPl=lvUYk-n
zWW+#OQMbLLz~03h^n~i87E$l=0X3JKa8|D56Um$15UJZ}>*%`?QNRF=N@2ySu_BZ`
zkC)s8O)l9kP0e76eWcP;c5Kpu3t9zIty1a@fdHvQNvB6j<n>62Q68}nM+*3Zkfo#m
zMM{ta2CegmGYMk#-~l%Ep}!uMN}i}yH6b)eUcGoU0$M$!bX!V*p9X=XBiTVnkr}{(
zL3hH-Qm^hy7eFPU3yGP<b(Ai|LuFMGeL!n4CRzoPh#>4?`eN_^G9JLlWcd=ik2b<7
zA(e1|u?iSK?ImrIEk)HXhXL?hTIa}c^3x<YFfM=etks38RyT6!$caTO<bWF)4@M&o
zq?P6)4Yx`XHHl<K1EEF86n}L-VNirEOejN!A*&4S?HDFcMTR98)EULbu1oPBqggZ?
z5@HMiB3xFU>2J>fBX?z`P2L<OBHL@h3icf->#!OvW)ivLg$5Tf94ze$IYoF7R^ah#
z6aYD?jte1ls9|zl0py9ipn*oG#UEEj-X;_4Q}kX<3@0Fiy=%Qv7`W{Pe|Sa6_~~}e
zBFtS&eh^(d+gwrF*Ady%G1G=&V3ZxCuUbz5+JqF0H7-Jx`2kzKsG*_=lXM&KC<q`(
z2v1(!6fK*klJh1tI>nJ{tDf6Irlqsivup=24de~#n1>O;Lmh#&sOxGL0Z7Zl!i^9z
z@|6QXs4MF895GI6^bgM-7MBWsAUH=t=%;6ckV_(&@oBjupZ|7TMvx4=kholi9KX$_
zAMOS}ponlQwKzH&>&Lq}sU{>oy$X`wGTQvsH!H#phP>|<zwO0S7y|_F`UUaM`Bv@&
zxW9l&!9WP7O?v**?*O4UF@;Xwma6E1|J_hBxE1aTfeBP5fKNM%5N&F(#O!A<UP(k@
z0@Ha_gf%q8b_5DZY`~+WR9z7)QV|spXfLoIBcdCLdu+gyVIv!ucI1?5r#jOE(-Zp}
zN&+^ckF<&efI1}O86N(eDXyZXJd4>N4-!ed%_y&x4=g@D6eze~W_T>*s^2O*(D18c
z1Gts402E0^Mr?w?f=B8bYdA-zwV<@%!@Tn5c7pvca5p}!!Yvjc$wje1Q+tqRI(T+V
zh+{Z_?b@})#c-b@9N{G!Kj~6XFIBZPz)J$j_82a<l;G6<-Giak8cNe*X17xX<>E@w
z5_6O_5WfjP-slNrHsS$LD^3+^Qx!qesI37+Thg9{WH5^Qe{T_YqCYn6eze3msI0iF
z1ZYzZkTV!vf?$x)LaK~98KPyVZv!wYpS=qsf>{wKTIOw~j-aPmMvjQo4sk2miQ$~~
z5J?cL?*dL}@hdyim=u+w5b$0rWI@Xxw^w_+c*K|5;kUUT_a<gy6w)k;KL&SmaGkn1
z2B2Lk4GDlLkXtatk=qlxCG`Zrq}NK{2Ex+(ZY{@rJ&<6?5U@bBPOrsk;AFh|6i|As
zi!ht*?nj~2s2gi1a>)cW8JJ_O74qTd6{GlsEY|ip@i$Ofy(h4kq$sLtSeW8W-av+-
zx}n%^q+|AES{69Vmq-F9IjQ6mVo-+l(N9Cv*3ZyClh;897_hc0m<@R+_MgZviVkrj
z=txb3LB6ahbd-{x>LBL;HykH8SH-+GEI%z}D{<s+2HFz{F9Lw7y2H@E3{P%6)WX5B
zDNpLY+<db|y*0vV6V?pF`(xHk#Z{hGJV8W6XKho1O2IXRyXj*=v-JJh2|V@5Kls#Z
zF;ks~tW1S7CN1G~-n-R>f7XPj%G%RfMW;NCYFrSsY%1RKpr}f=n%kc(4AKE;sv^m^
zjSVJL?QkH@$L&H{bWm;WwR#GGq4aGqY8D`&2dSNVt<UPt6#ZX!O?)Px*B)qjgWYV0
z$^v9+e>NXbOB<(eXY_W`K00dOeMXcnfe?{`gq`+Qc+xXzzXY;@z^r$f-j7~vi(~bR
zMWGMIRTo@ff$PN78DMVrIci~qN}~OrGux+J5Tf|bkocvD<f0LvBa*8P_j>K!K^_fi
zU7awq(ZNIY(qn`!iwR2TY6X|FfdVxOq8ilB)9#tnGo;XJgAO%_;O)ypVziPL%7RJ+
zXtD@T?tO3}L85LBfNGA`I7MkWdAD=G@L*W6<+(7qwh%RRXo8Kv<Y%v5u%v|wT?_d}
z{#5eNSOQ&&H8dVU1SDztheBN*ig*Q;EmU9Itk9REVh!+*u_4KI8ILR3z<d*-Pb5TC
zFbL5T6)N5J9Fmb*n0pj4ZKK{cS002oaO)SsFd$A5d+1wpSExHmQ-(x4-t>AUmlo%5
zdi|1I;Fmwjt|zz1Ec2w6Qu(R=B1p)Tr>{w*esnk?V}DE(^JK~|iNXl?U3xzE@kQ<L
zczd0IjpPHY!#@jneZQ6tMT!Ydt!-x|v|8Xp%PuOCTYIO_gI250S(_46NMWv8W6_HH
z_Is;4X{5p-c`*7EF)-W|(om<i*l2Cf#a>ATH7TL)PftPnSmLyT^4YX4PAb%pc3`wG
zy{d({GC5eZt4jO6#@Ydq+C`S8blX_BQ~A=@KOHfCv|FGd&aG(S49?%-%33(4Qnhw7
zN~&8yC@T2-V@5rD28Gnub{^Qy-wog%8IwpP(JB~kASDfK0Xk34(xDReL8-{pe{&>Z
z2qrLQxUFr3ir1DI>YB9CiS5;h(*~S+L%V7FqSlu<N+IhrN{#V^sP+zNotGdcqwbLs
z$CSGPVXm$n&JYx&iPpqR?Qmq6u%*00P$Ooa+P26$Ye1GD!cl?qkTR4uI3)G-0Yb_X
zO5_`ibpMz<=KJh<C(rtu<T3wLX8tyL%=g*zP4Y}panXjFcc-RjWLvk+gfXxp6#}&e
zt4~@cWInE5iF}vbv$}-wNQO4`HBrzVNX{!05qu|6LNh|qVkKts;aS4ZcVke&WFNAk
z+^KLQm_<d=LL@T6YSU7H_Nod^!$`{BI7F2m4w0ynNpB@nv)1B~{8vhj<)(Ij!Qra5
ztii(6@X<a{1aC6l>Y=-~Io*G`UGKdHNu|&Z-=lF!8W7J(1OiQgmB$K%+_P-}1H87&
zoo9nXx5Pn%#|E>p1`tXZt4Z|`$saf;p={4X<<E-%36#Jqut80p+s4syM$wXSx0I-+
zh17E%!t)E;(N^eBzN7y>LZTDpj=<vpcqCP$sc4|}NAfbYD#BA?1R9JcL?pddaM5s0
zYb%Ts3nNZ|TCIPAymsjQH|xkm>eic_)!mrCmXw0_W9*Hg1Y4teElmecKvr`*vVCh6
z*3P&f381qwJ#TP`@N4-lS{<~(!(2Fw;L9b1B=kW)Yz~+WGe9oC$bF3`ME{rD_I~;l
zV`Em=zc24v_1Wd!$G1%mg5RQtB9J5bQM3v5MP9EQ11nATB%sA6RChAeBUNF$&5Lr6
zxv<Q&6;=N|(v{Jky=9bPw0CO@4mKYi#&eW1<h6Q#N`9rjpxUY{6*fev59hUb)D$6D
z72d4A4Y@Y8bVz*!2jaOKKp#rW`>Cp1woOC5e4Kro_*&10@?@Xg-5^T(#(*R&F`A2N
zm(tph5k!K7hxSfV7Mij{*;?_xy#%G_C9@Mj`nl-?+%}IWa18x?h$-&}uPsyq?Tu!d
zD?dB4C>y|sdc%dtXfWNqB7yQO)D24CarVFRIiK1<zeZSQt#NmFCyR2X5HkLC+mwU3
zX~PFv&!J<r6U&PlK1kihWxNx{t5y6BJ<hAE9n7eY*Cs0>KXS)}(w%r)_FA=pxg1z(
z&AnZdg{A(u%C+|2gOY~7k>&kv6oZC7tu^?iKd3GMK`y}<aUw0c)mHBl+TAJB{noZo
z?W6X6n@uRx#kGiL{@%$03lpV1ptD+@9un!mZ1YiF1YAn~YUgwx+BS8j3siz-vUcnT
z<Yv&iFiJ+dfQIyBS(S8m<cJedPzDiGQQiKXTY_&<b&lA4U$&WOVYnVA0~84S($xX%
zXzQ+iY^qJW=a`Ik$h{i&6u1f^%;B`BoJ^}VW^-Fr5GAxS@h6_Gid(#<j}*B3@0}W#
zaIXz|j2N2!14d+aReg(%-k}^C7)P3rBy~`b2OS5#meNqkUMsQa91ATC$N>GH0azJ%
zhc}5oKftBoB)s5|+5}DA-7;<;!!EtRJFn)EmS>raTHR2mu`Qm$9hO{@vm#@z)(_&%
zXpfJ&;V3eB^75k+?mj}BdMuWih#zUcM$`r}o<7l$+QsCk{$NPjlwT7;qE~4w<quVo
zl*E##>Hh;SXnhLZ5|&E<00D$)LqkwWLqi~Na&Km7Y-Iodc$|HaJxIeq9K~N#r9~<V
z78G&FP@OD@is+_QC_;r$E41oha{YuR4M~cNqu^R_@ME#+;Nq;SgR3A2et<YTI4QbF
ziT_IqEn+-4?#H`(kGpq(P%ksp>=*-7%`%eln3&G2ih)-M=tnPR5R{pz&q-1Op5yBt
zKEB>Xd6xINKSy^qXEMMi63;T-u!uK^r#3B}^FDEy73D1PIq|4L7bJe<y5jO1=Yqom
z&kP%><UDbhSS+-$(#EW4sKk@Rk*umwzL0h~%Xy2lTCTF@J^2emIc;T`>okWD$0C*>
zL4<-TN+`oZlva%t6G_^SdH4q%zeFyDTqQ7aET94vvf~H;gWuhn`H68iDHH=bUu^qh
z7zpeFjhbzLAKP~01n@rtS6a(osRPrWq}N(n<Ot~51}?5!nz9F6?f?T%x@^df<fkd*
z^T7KVeNzVLy9K(|+}@h|IDG(8)K%gJI5-4G3Y5L>@$O)IZ~vZY_V)vQNph=3<Nuog
z000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2j~J95eF5%wX|XY00m1)L_t(o!_Ajl
zY?MV5$IqFWZ|~dNZo6B`r4$QDE0jPR5K#<9(FE{<iSfaZK*S3%^~DEbq8NiFm>^Gv
z2Ly#gA0QAhhD3ZI;DwZ88=$o<g<ha7W!vrUcG=7K&5RG-r5AR$-(D!4r`?&!`F?Ze
zocW)FeSLkPd$a}FKS6p=Lx%8_NvzFNEmTg~6XwxNp0>t7w<?8$S=JU-RuwGJ{|VFy
zBmqu%j6aUjiIYx*eeQ6nQ9PI#2n~OA>+E&aKXKbOd&SQ3jf_w|A0j%zFODcvXaK+?
zVc(!JEO6|vCisD_->1+3z=^=ka|2B}KcowYOZeWeBwXs^_-}xX{ciDfH%7e)w;l8}
zhIuq+Kwg!2MAFsPaLh>0{h?%gR4MxJ59jjAQj9?S%0@*h9|Y9<IRmmWsW0$l_Q%r&
zWFm6Ajl+x#y;ruWFj~XpECRApxu-O|!=_pB(E>63Tg_Cy>Wpr2M2FN#$58Mw0mWR3
zP;3x0;UP_eS0$>BI6(jfp~xVzd^18?B%p>6q0nG|AS^MUKYhv(pK>>ylP@50dx5q(
zPt5=&sD*#)Z#*=7b?mR37~U<Zd#0p{AQAxhb;$6wH$55v=oYwlSUKl4q)QX#(JwoG
zc|ZdI10wpxz{Q_>E&u@O4ADaYma*Fp`dEJ?y{kLhf2ARsKt_&?wEXAmC6}_G85_;P
z^ep`BZArHDx3`1b<Jo3yd)z|^)0^^yeV*i!=TU<sdvz^}7maq3DxUHU)@k*Q0FoMW
zwj#CE6ibH+n=9EK7cscaMbnyEVV_HC^Rzi6bCFuXWQ{$R`HS=BK4(hIw5z1fNR`y4
z+{C!nxs;fQ{M;2;NHbxoVY1#8Ve!bJNItIEw8rS1vh5@1%Jr`5)TZW9^ZrF!tjrWE
z^1}kJ1XxR$1cj~`&m*$hENblP!$PJBes!d)+27;Ug7Zy|HAR(WmT^RHByhC%{3MAa
zhH6{cYDcAs8q(-GW^53cWh4HkaUt4o$s0YuCn*ToxCZUk@gmjhm)F{**AIxeq?}Zi
z117j);_oE>c91${j!Nr1Ua$-Zivh{eAm4vXp1G00^tu{|1Br&Ky#6?Z#-9oC+7ep-
z_C#XTg6!5&;y;zHW2kYJR#7i3r35t2_a072GjWL4PQL4hi8KHpPqp!Hj)OKn$!;FW
zfCd0C(#SeKLoGx^Q;YngXZl|8z{$j&#+eRry(8vM-whHtopJP_c9RbNMa1RS>HC{|
z!*F>p>1uO(JapQ!O)*~sfq;(wE#=!J@5XBhsb&d;k!FIWj#N8r)^t+Vo+Xg5tRuL(
zDk;s*S}>dRo8tx5*#Z?kO~}#;+`TTrV!4IBwJ9xg(&FOoPco;;d!0L0FaTihZ02r<
z`1B0Kixu3un0~Mgi}KSos+GM6)J*%o12LE$jq`nV-c~@$000=HuRKdP)QR3ch(ys<
zj4nsU2i4r`wc=VbdK-C2UnEmun=cjHLye-vM3z+KMv$mU<UcV(oomXd4xt#&d`T3b
z=En)7+&)b_hbOzLj62@sb6${CZ?-<-@`K-@gWoExdr=L{qIw7;4XpR%>?+WR_c*Gf
zv#Vmn;GNt7rEW=V=6G7A)H1?y284zAFyE4Nb=krgka<puGf9g(cOGJte3(Sn%B{3+
zc`T(0tEBQ|;%qD4Etxf!F+`uQ!yT)p&MvgFgYS)31k|W4+5^P>kZQi6&E2dw)Y9n7
zB#6dq;$pKr)`Ybs_|i(Up%x3L3NS|f^zM1$yMU3)()u!YZsd;TfO=ox{{hy@{Zc^e
Rp-lh)002ovPDHLkV1gFt9c};s

literal 0
HcmV?d00001


From 365f9d32327aca9a0145e0f59bc947da991539d4 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 24 Feb 2024 06:59:10 -0800
Subject: [PATCH 096/131] runtime update

---
 .../org/myrobotlab/framework/CmdOptions.java  |  2 +-
 .../java/org/myrobotlab/service/Runtime.java  | 21 ++++++++-----------
 2 files changed, 10 insertions(+), 13 deletions(-)

diff --git a/src/main/java/org/myrobotlab/framework/CmdOptions.java b/src/main/java/org/myrobotlab/framework/CmdOptions.java
index 2c357e8db6..3b3b18d7bb 100644
--- a/src/main/java/org/myrobotlab/framework/CmdOptions.java
+++ b/src/main/java/org/myrobotlab/framework/CmdOptions.java
@@ -39,7 +39,7 @@ static boolean contains(List<String> l, String flag) {
 
   // launcher
   @Option(names = { "-c",
-      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config data/config/my-config-dir")
+      "--config" }, fallbackValue = "default", description = "Specify a configuration set to start. The config set is a directory which has all the necessary configuration files. It loads runtime.yml first, and subsequent service configuration files will then load. \n example: --config my-config-dir to start the configuration stored in config data/config/my-config-dir")
   public String config = null;
 
   @Option(names = { "-h", "-?", "--help" }, description = "shows help")
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 9b76d3bf65..e203a65b81 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -433,11 +433,6 @@ private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, M
       RuntimeConfig currentConfig = Runtime.getInstance().config;
 
       for (String service : plansRtConfig.getRegistry()) {
-        // FIXME - determine if you want to return a complete merge of activated
-        // or just "recent"
-        if (Runtime.getService(service) != null) {
-          continue;
-        }
         ServiceConfig sc = plan.get(service);
         if (sc == null) {
           runtime.error("could not get %s from plan", service);
@@ -915,12 +910,14 @@ public static Runtime getInstance() {
             runtime.apply(c);
           }
 
-          if (options.services != null) {
+          if (options.services != null && options.services.size() != 0) {
             log.info("command line override for services created");
             createAndStartServices(options.services);
           } else {
             log.info("processing config.registry");
-            if (startYml.enable) {
+            if (options.config != null) {
+              Runtime.startConfig(options.config);
+            } else if (startYml.enable) {
               Runtime.startConfig(startYml.config);
             }
           }
@@ -1549,7 +1546,7 @@ static public void install(String serviceType, Boolean blocking) {
       if (blocking == null) {
         blocking = false;
       }
-      
+
       if (installerThread != null) {
         log.error("another request to install dependencies, 1st request has not completed");
         return;
@@ -1576,7 +1573,7 @@ public void run() {
       } else {
         installerThread.start();
       }
-      
+
       installerThread = null;
     }
   }
@@ -4918,16 +4915,16 @@ static public void releaseConfigPath(String configPath) {
       RuntimeConfig config = CodecUtils.fromYaml(releaseData, RuntimeConfig.class);
       List<String> registry = config.getRegistry();
       Collections.reverse(Arrays.asList(registry));
-      
+
       // get starting services if any entered on the command line
-      // -s log Log webgui WebGui ... etc - these will be protected 
+      // -s log Log webgui WebGui ... etc - these will be protected
       List<String> startingServices = new ArrayList<>();
       if (options.services.size() % 2 == 0) {
         for (int i = 0; i < options.services.size(); i += 2) {
           startingServices.add(options.services.get(i));
         }
       }
-      
+
       for (String name : registry) {
         if (startingServices.contains(name)) {
           continue;

From 000ed4d765f2b83fd1fc96ff99a7f692a64b3649 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 24 Feb 2024 07:46:43 -0800
Subject: [PATCH 097/131] reverting bad git merge

---
 .../java/org/myrobotlab/service/InMoov2.java  | 374 +++++-------------
 1 file changed, 101 insertions(+), 273 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 2f2b513e87..c496da1a61 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -14,6 +14,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.commons.io.FilenameUtils;
 import org.myrobotlab.framework.Message;
@@ -22,7 +23,6 @@
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Registration;
 import org.myrobotlab.framework.Service;
-import org.myrobotlab.framework.StaticType;
 import org.myrobotlab.framework.Status;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.io.FileIO;
@@ -37,7 +37,6 @@
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
-import org.myrobotlab.service.config.OpenCVConfig;
 import org.myrobotlab.service.data.JoystickData;
 import org.myrobotlab.service.data.Locale;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -46,14 +45,66 @@
 import org.myrobotlab.service.interfaces.ServiceLifeCycleListener;
 import org.myrobotlab.service.interfaces.ServoControl;
 import org.myrobotlab.service.interfaces.Simulator;
+import org.myrobotlab.service.interfaces.SpeechListener;
 import org.myrobotlab.service.interfaces.SpeechRecognizer;
-import org.myrobotlab.service.interfaces.SpeechSynthesis;
 import org.myrobotlab.service.interfaces.TextListener;
 import org.myrobotlab.service.interfaces.TextPublisher;
 import org.slf4j.Logger;
 
-public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleListener, TextListener, TextPublisher,
-    JoystickListener, LocaleProvider, IKJointAngleListener {
+public class InMoov2 extends Service<InMoov2Config>
+    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
+
+  public class Heart implements Runnable {
+    private final ReentrantLock lock = new ReentrantLock();
+    private Thread thread;
+
+    @Override
+    public void run() {
+      if (lock.tryLock()) {
+        try {
+          while (!Thread.currentThread().isInterrupted()) {
+            invoke("publishHeartbeat");
+            Thread.sleep(config.heartbeatInterval);
+          }
+        } catch (InterruptedException ignored) {
+          Thread.currentThread().interrupt();
+        } finally {
+          lock.unlock();
+          log.info("heart stopping");
+          thread = null;
+        }
+      }
+    }
+
+    public void start() {
+      if (thread == null) {
+        log.info("starting heart");
+        thread = new Thread(this, String.format("%s-heart", getName()));
+        thread.start();
+        config.heartbeat = true;
+      } else {
+        log.info("heart already started");
+      }
+    }
+
+    public void stop() {
+      if (thread != null) {
+        thread.interrupt();
+        config.heartbeat = false;
+      } else {
+        log.info("heart already stopped");
+      }
+    }
+  }
+
+  public static class Heartbeat {
+    public long count = 0;
+    public long ts = System.currentTimeMillis();
+
+    public Heartbeat(InMoov2 inmoov) {
+      this.count = inmoov.heartbeatCount;
+    }
+  }
 
   public final static Logger log = LoggerFactory.getLogger(InMoov2.class);
 
@@ -63,7 +114,7 @@ public class InMoov2 extends Service<InMoov2Config> implements ServiceLifeCycleL
    * This method will load a python file into the python interpreter.
    * 
    * @param file
-   *             file to load
+   *          file to load
    * @return success/failure
    */
   @Deprecated /* use execScript - this doesn't handle resources correctly */
@@ -121,77 +172,6 @@ public static void main(String[] args) {
         return;
       }
 
-      OpenCVConfig ocvConfig = i01.getPeerConfig("opencv", new StaticType<>() {
-      });
-      ocvConfig.flip = true;
-      i01.setPeerConfigValue("opencv", "flip", true);
-      // i01.savePeerConfig("", null);
-
-      // Runtime.startConfig("default");
-
-      // Runtime.main(new String[] { "--log-level", "info", "-s", "webgui",
-      // "WebGui",
-      // "intro", "Intro", "python", "Python" });
-
-      Runtime.start("python", "Python");
-      // Runtime.start("ros", "Ros");
-      Runtime.start("intro", "Intro");
-      // InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-      // i01.startPeer("simulator");
-      // Runtime.startConfig("i01-05");
-      // Runtime.startConfig("pir-01");
-
-      // Polly polly = (Polly)Runtime.start("i01.mouth", "Polly");
-      // i01 = (InMoov2) Runtime.start("i01", "InMoov2");
-
-      // polly.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-      // i01.startPeer("mouth");
-      // i01.speakBlocking("Hi, to be or not to be that is the question,
-      // wheather to take arms against a see of trouble, and by aposing them end
-      // them, to sleep, to die");
-
-      Runtime.start("python", "Python");
-
-      // i01.startSimulator();
-      Plan plan = Runtime.load("webgui", "WebGui");
-      // WebGuiConfig webgui = (WebGuiConfig) plan.get("webgui");
-      // webgui.autoStartBrowser = false;
-      Runtime.startConfig("webgui");
-      Runtime.start("webgui", "WebGui");
-
-      Random random = (Random) Runtime.start("random", "Random");
-
-      random.addRandom(3000, 8000, "i01", "setLeftArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightArmSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveLeftArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-      random.addRandom(3000, 8000, "i01", "moveRightArm", 0.0, 5.0, 85.0, 95.0, 25.0, 30.0, 10.0, 15.0);
-
-      random.addRandom(3000, 8000, "i01", "setLeftHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-      random.addRandom(3000, 8000, "i01", "setRightHandSpeed", 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0, 8.0, 25.0,
-          8.0, 25.0);
-
-      random.addRandom(3000, 8000, "i01", "moveRightHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          130.0, 175.0);
-      random.addRandom(3000, 8000, "i01", "moveLeftHand", 10.0, 160.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0, 10.0, 60.0,
-          5.0, 40.0);
-
-      random.addRandom(200, 1000, "i01", "setHeadSpeed", 8.0, 20.0, 8.0, 20.0, 8.0, 20.0);
-      random.addRandom(200, 1000, "i01", "moveHead", 70.0, 110.0, 65.0, 115.0, 70.0, 110.0);
-
-      random.addRandom(200, 1000, "i01", "setTorsoSpeed", 2.0, 5.0, 2.0, 5.0, 2.0, 5.0);
-      random.addRandom(200, 1000, "i01", "moveTorso", 85.0, 95.0, 88.0, 93.0, 70.0, 110.0);
-
-      random.save();
-
-      // i01.startChatBot();
-      //
-      // i01.startAll("COM3", "COM4");
-      Runtime.start("python", "Python");
-
     } catch (Exception e) {
       log.error("main threw", e);
     }
@@ -266,8 +246,7 @@ public static void main(String[] args) {
 
   public InMoov2(String n, String id) {
     super(n, id);
-    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
-        "pt-PT", "tr-TR");
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -281,8 +260,7 @@ public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
 
-      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT",
-          "fi-FI", "pt-PT", "tr-TR");
+      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
 
       if (c.locale != null) {
         setLocale(c.locale);
@@ -657,9 +635,9 @@ public void displayFullScreen(String src) {
       error("could not display picture %s", src);
     }
   }
-
+  
   public void enableRandomHead() {
-    Random random = (Random) getPeer("random");
+    Random random = (Random)getPeer("random");
     if (random != null) {
       random.disableAll();
       random.enable(String.format("%s.setHeadSpeed", getName()));
@@ -667,12 +645,12 @@ public void enableRandomHead() {
       random.enable();
     }
   }
-
+  
   public void disableRandom() {
-    Random random = (Random) getPeer("random");
+    Random random = (Random)getPeer("random");
     if (random != null) {
       random.disable();
-    }
+    }    
   }
 
   public void enable() {
@@ -705,7 +683,7 @@ public boolean exec(String pythonCode) {
    * This method will try to launch a python command with error handling
    * 
    * @param gesture
-   *                the gesture
+   *          the gesture
    * @return gesture result
    */
   public String execGesture(String gesture) {
@@ -740,7 +718,7 @@ public void execScript() {
    * a filesystem file :P
    * 
    * @param someScriptName
-   *                       execute a resource script
+   *          execute a resource script
    * @return success or failure
    */
   public void execScript(String someScriptName) {
@@ -818,18 +796,11 @@ public InMoov2Head getHead() {
    */
   public Long getLastActivityTime() {
     Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
-    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime()
-        : null;
-    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime()
-        : null;
-    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null
-        ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime()
-        : null;
-    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null
-        ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime()
-        : null;
-    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime()
-        : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
 
     Long lastActivityTime = null;
 
@@ -989,7 +960,7 @@ public void loadGestures() {
    * file should contain 1 method definition that is the same as the filename.
    * 
    * @param directory
-   *                  - the directory that contains the gesture python files.
+   *          - the directory that contains the gesture python files.
    * @return true/false
    */
   public boolean loadGestures(String directory) {
@@ -1088,8 +1059,7 @@ public void moveHand(String which, Double thumb, Double index, Double majeure, D
     moveHand(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void moveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     invoke("publishMoveHand", which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1141,10 +1111,8 @@ public void moveLeftHand(Double thumb, Double index, Double majeure, Double ring
     moveHand("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveLeftHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveRightArm(Double bicep, Double rotate, Double shoulder, Double omoplate) {
@@ -1155,10 +1123,8 @@ public void moveRightHand(Double thumb, Double index, Double majeure, Double rin
     moveHand("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void moveRightHand(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    moveHand("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public void moveTorso(Double topStom, Double midStom, Double lowStom) {
@@ -1187,7 +1153,7 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
    * comes in from runtime which owns the config list
    * 
    * @param configList
-   *                   list of configs
+   *          list of configs
    */
   public void onConfigList(List<String> configList) {
     this.configList = configList;
@@ -1210,7 +1176,7 @@ public void onEndSpeaking(String utterance) {
    * including lower level logs that do not propegate as statuses
    * 
    * @param log
-   *            - flushed log from Log service
+   *          - flushed log from Log service
    */
   public void onErrors(List<LogEntry> log) {
     errors.addAll(log);
@@ -1255,23 +1221,6 @@ public void onJoystickInput(JoystickData input) throws Exception {
     invoke("publishEvent", "joystick");
   }
 
-  /**
-   * Centralized logging system will have all logging from all services,
-   * including lower level logs that do not propegate as statuses
-   * 
-   * @param log
-   *            - flushed log from Log service
-   */
-  public void onLogEvents(List<LogEntry> log) {
-    // scan for warn or errors
-    for (LogEntry entry : log) {
-      if ("ERROR".equals(entry.level) && errors.size() < 100) {
-        errors.add(entry);
-        // invoke("publishError", entry);
-      }
-    }
-  }
-
   public String onNewState(String state) {
     log.error("onNewState {}", state);
 
@@ -1305,8 +1254,7 @@ public void onPeak(double volume) {
 
   public void onPirOff() {
     log.info("onPirOff");
-    setPredicate("pir", true);
-    setPredicate("pir.off", System.currentTimeMillis());
+    setPredicate(String.format("%s.pir_off", getName()), System.currentTimeMillis());
     processMessage("onPirOff");
   }
 
@@ -1316,8 +1264,7 @@ public void onPirOff() {
    */
   public void onPirOn() {
     log.info("onPirOn");
-    setPredicate("pir", false);
-    setPredicate("pir.on", System.currentTimeMillis());
+    setPredicate(String.format("%s.pir_on", getName()), System.currentTimeMillis());
     processMessage("onPirOn");
   }
 
@@ -1468,7 +1415,7 @@ public void processMessage(String method) {
    * @param method
    * @param data
    */
-  public void processMessage(String method, Object... data) {
+  public void processMessage(String method, Object ... data) {
     // User processing should not occur until after boot has completed
     if (!state.equals("boot")) {
       // FIXME - this needs to be in config
@@ -1578,8 +1525,7 @@ public Heartbeat publishHeartbeat() {
       }
 
       // interval event firing
-      if (config.stateRandomInterval != null
-          && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
         // fsm.fire("random");
         stateLastRandomTime = System.currentTimeMillis();
       }
@@ -1631,8 +1577,7 @@ public Message publishMessage(Message msg) {
     return msg;
   }
 
-  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder,
-      Double omoplate) {
+  public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double rotate, Double shoulder, Double omoplate) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("bicep", bicep);
     map.put("rotate", rotate);
@@ -1646,8 +1591,7 @@ public HashMap<String, Double> publishMoveArm(String which, Double bicep, Double
     return map;
   }
 
-  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure,
-      Double ringFinger, Double pinky, Double wrist) {
+  public HashMap<String, Object> publishMoveHand(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Object> map = new HashMap<>();
     map.put("which", which);
     map.put("thumb", thumb);
@@ -1664,8 +1608,7 @@ public HashMap<String, Object> publishMoveHand(String which, Double thumb, Doubl
     return map;
   }
 
-  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw,
-      Double rollNeck) {
+  public HashMap<String, Double> publishMoveHead(Double neck, Double rothead, Double eyeX, Double eyeY, Double jaw, Double rollNeck) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("neck", neck);
     map.put("rothead", rothead);
@@ -1685,8 +1628,7 @@ public HashMap<String, Double> publishMoveLeftArm(Double bicep, Double rotate, D
     return map;
   }
 
-  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveLeftHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1706,8 +1648,7 @@ public HashMap<String, Double> publishMoveRightArm(Double bicep, Double rotate,
     return map;
   }
 
-  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky, Double wrist) {
+  public HashMap<String, Double> publishMoveRightHand(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     HashMap<String, Double> map = new HashMap<>();
     map.put("thumb", thumb);
     map.put("index", index);
@@ -1793,7 +1734,7 @@ public StateChange publishStateChange(StateChange stateChange) {
 
     lastState = state;
     state = stateChange.state;
-
+    
     setPredicate(String.format("%s.end", lastState), System.currentTimeMillis());
     setPredicate(String.format("%s.start", state), System.currentTimeMillis());
 
@@ -1889,8 +1830,7 @@ public void setAutoDisable(Boolean param) {
   }
 
   @Override
-  public void setConfigValue(String fieldname, Object value)
-      throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
+  public void setConfigValue(String fieldname, Object value) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
     super.setConfigValue(fieldname, value);
     setPredicate(fieldname, value);
   }
@@ -1899,8 +1839,7 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
-  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandSpeed(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     InMoov2Hand hand = getHand(which);
     if (hand == null) {
       warn("%s hand not started", which);
@@ -1910,14 +1849,12 @@ public void setHandSpeed(String which, Double thumb, Double index, Double majeur
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger,
-      Double pinky) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, null);
   }
 
   @Deprecated
-  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setHandVelocity(String which, Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed(which, thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
@@ -1933,8 +1870,7 @@ public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double e
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, null);
   }
 
-  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadSpeed(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     sendToPeer("head", "setSpeed", rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1958,8 +1894,7 @@ public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Doubl
   }
 
   @Deprecated
-  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed,
-      Double rollNeckSpeed) {
+  public void setHeadVelocity(Double rothead, Double neck, Double eyeXSpeed, Double eyeYSpeed, Double jawSpeed, Double rollNeckSpeed) {
     setHeadSpeed(rothead, neck, eyeXSpeed, eyeYSpeed, jawSpeed, rollNeckSpeed);
   }
 
@@ -1971,15 +1906,12 @@ public void setLeftArmSpeed(Integer bicep, Integer rotate, Integer shoulder, Int
     setArmSpeed("left", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setLeftHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("left", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setLeftHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("left", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   @Override
@@ -2034,15 +1966,12 @@ public void setRightArmSpeed(Integer bicep, Integer rotate, Integer shoulder, In
     setArmSpeed("right", (double) bicep, (double) rotate, (double) shoulder, (double) omoplate);
   }
 
-  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky,
-      Double wrist) {
+  public void setRightHandSpeed(Double thumb, Double index, Double majeure, Double ringFinger, Double pinky, Double wrist) {
     setHandSpeed("right", thumb, index, majeure, ringFinger, pinky, wrist);
   }
 
-  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky,
-      Integer wrist) {
-    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky,
-        (double) wrist);
+  public void setRightHandSpeed(Integer thumb, Integer index, Integer majeure, Integer ringFinger, Integer pinky, Integer wrist) {
+    setHandSpeed("right", (double) thumb, (double) index, (double) majeure, (double) ringFinger, (double) pinky, (double) wrist);
   }
 
   public boolean setSpeechType(String speechType) {
@@ -2142,107 +2071,6 @@ public void speakBlocking(String format, Object... args) {
     }
   }
 
-  @Deprecated /* use startPeers */
-  public void startAll() throws Exception {
-    startAll(null, null);
-  }
-
-  @Deprecated /* use startPeers */
-  public void startAll(String leftPort, String rightPort) throws Exception {
-    startMouth();
-    startChatBot();
-
-    // startHeadTracking();
-    // startEyesTracking();
-    // startOpenCV();
-    startEar();
-
-    startServos();
-    // startMouthControl(head.jaw, mouth);
-
-    speakBlocking(get("STARTINGSEQUENCE"));
-  }
-
-  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
-  public void startBrain() {
-    startChatBot();
-  }
-
-  @Deprecated /* i01.startPeer("chatBot") - all details should be in config */
-  public ProgramAB startChatBot() {
-
-    try {
-      chatBot = (ProgramAB) startPeer("chatBot");
-
-      if (locale != null) {
-        chatBot.setCurrentBotName(locale.getTag());
-      }
-
-      // FIXME remove get en.properties stuff
-      speakBlocking(get("CHATBOTACTIVATED"));
-
-      chatBot.attachTextPublisher(ear);
-
-      // this.attach(chatBot); FIXME - attach as a TextPublisher - then
-      // re-publish
-      // FIXME - deal with language
-      // speakBlocking(get("CHATBOTACTIVATED"));
-      chatBot.repetitionCount(10);
-      // chatBot.setPath(getResourceDir() + fs + "chatbot");
-      // chatBot.setPath(getDataDir() + "ProgramAB");
-      chatBot.startSession("default", locale.getTag());
-      // reset some parameters to default...
-      chatBot.setPredicate("topic", "default");
-      chatBot.setPredicate("questionfirstinit", "");
-      chatBot.setPredicate("tmpname", "");
-      chatBot.setPredicate("null", "");
-      // load last user session
-      if (!chatBot.getPredicate("name").isEmpty()) {
-        if (chatBot.getPredicate("lastUsername").isEmpty() || chatBot.getPredicate("lastUsername").equals("unknown")
-            || chatBot.getPredicate("lastUsername").equals("default")) {
-          chatBot.setPredicate("lastUsername", chatBot.getPredicate("name"));
-        }
-      }
-      chatBot.setPredicate("parameterHowDoYouDo", "");
-      chatBot.savePredicates();
-      htmlFilter = (HtmlFilter) startPeer("htmlFilter");// Runtime.start("htmlFilter",
-      // "HtmlFilter");
-      chatBot.attachTextListener(htmlFilter);
-      htmlFilter.attachTextListener((TextListener) getPeer("mouth"));
-      chatBot.attachTextListener(this);
-      // start session based on last recognized person
-      // if (!chatBot.getPredicate("default", "lastUsername").isEmpty() &&
-      // !chatBot.getPredicate("default", "lastUsername").equals("unknown")) {
-      // chatBot.startSession(chatBot.getPredicate("lastUsername"));
-      // }
-      if (chatBot.getPredicate("default", "firstinit").isEmpty()
-          || chatBot.getPredicate("default", "firstinit").equals("unknown")
-          || chatBot.getPredicate("default", "firstinit").equals("started")) {
-        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
-        invoke("publishEvent", "FIRST INIT");
-      } else {
-        chatBot.startSession(chatBot.getPredicate("default", "lastUsername"));
-        invoke("publishEvent", "WAKE UP");
-      }
-    } catch (Exception e) {
-      speak("could not load chatBot");
-      error(e.getMessage());
-      speak(e.getMessage());
-    }
-    broadcastState();
-    return chatBot;
-  }
-
-  @Deprecated /* use startPeer */
-  public SpeechRecognizer startEar() {
-
-    ear = (SpeechRecognizer) startPeer("ear");
-    ear.attachSpeechSynthesis((SpeechSynthesis) getPeer("mouth"));
-    ear.attachTextListener(chatBot);
-    broadcastState();
-    return ear;
-  }
-
   public void startedGesture() {
     startedGesture("unknown");
   }

From 81c13b0bffe47de2492afd971021c905a7d10317 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 24 Feb 2024 11:06:23 -0800
Subject: [PATCH 098/131] sliding window logs for py4j and python

---
 .../java/org/myrobotlab/service/Py4j.java     | 103 +++++++++++-------
 .../java/org/myrobotlab/service/Python.java   |  23 +++-
 src/main/resources/resource/Py4j/Py4j.py      |  70 ++++++------
 .../resource/WebGui/app/service/js/Py4jGui.js |  85 ++++++++-------
 .../WebGui/app/service/js/PythonGui.js        |  43 ++++----
 .../WebGui/app/service/views/Py4jGui.html     |  13 ++-
 .../WebGui/app/service/views/PythonGui.html   |  16 ++-
 7 files changed, 209 insertions(+), 144 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Py4j.java b/src/main/java/org/myrobotlab/service/Py4j.java
index 9e768854b7..b056ceb162 100644
--- a/src/main/java/org/myrobotlab/service/Py4j.java
+++ b/src/main/java/org/myrobotlab/service/Py4j.java
@@ -15,6 +15,7 @@
 import org.myrobotlab.framework.Message;
 import org.myrobotlab.framework.Platform;
 import org.myrobotlab.framework.Service;
+import org.myrobotlab.generics.SlidingWindowList;
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.io.StreamGobbler;
 import org.myrobotlab.logging.Level;
@@ -35,8 +36,9 @@
 /**
  * 
  * 
- * A bridge between a native proces of Python running and MRL.
- * Should support any version of Python. 
+ * A bridge between a native proces of Python running and MRL. Should support
+ * any version of Python.
+ * 
  * <pre>
  *  requirements: 
  * 
@@ -137,6 +139,11 @@ public void run() {
    */
   private transient Executor handler = null;
 
+  /**
+   * a sliding window of logs
+   */
+  protected List<String> logs = new SlidingWindowList<>(300);
+
   /**
    * Opened scripts are scripts opened in memory, from there they can be
    * executed or saved to the file system, or updatd in memory which the js
@@ -170,7 +177,7 @@ public Py4j(String n, String id) {
    *          - code block
    */
   public void addScript(String scriptName, String code) {
-    Py4jConfig c = (Py4jConfig)config;
+    Py4jConfig c = (Py4jConfig) config;
     File script = new File(c.scriptRootDir + fs + scriptName);
 
     if (script.exists()) {
@@ -181,10 +188,11 @@ public void addScript(String scriptName, String code) {
     openedScripts.put(scriptName, new Script(scriptName, code));
     broadcastState();
   }
-  
+
   /**
    * If autostartPython is true, Py4j will start a process on starting and
    * connect the stdout/stdin streams to be redirected to the UI
+   * 
    * @param b
    * @return
    */
@@ -192,14 +200,15 @@ public boolean autostartPython(boolean b) {
     config.autostartPython = b;
     if (config.autostartPython && pythonProcess == null) {
       startPythonProcess();
-    }    
+    }
     return b;
   }
 
   /**
    * removes script from memory of openScripts
    * 
-   * @param scriptName The name of the script to close.
+   * @param scriptName
+   *          The name of the script to close.
    */
   public void closeScript(String scriptName) {
     openedScripts.remove(scriptName);
@@ -234,7 +243,8 @@ public void connectionStopped(Py4JServerConnection gatewayConnection) {
   /**
    * One of 3 methods supported on the MessageHandler() callbacks
    * 
-   * @param code The Python code to execute in the interpreter.
+   * @param code
+   *          The Python code to execute in the interpreter.
    */
   @Override
   public boolean exec(String code) {
@@ -256,7 +266,6 @@ private String getClientKey(Py4JServerConnection gatewayConnection) {
     return String.format("%s:%d", gatewayConnection.getSocket().getInetAddress(), gatewayConnection.getSocket().getPort());
   }
 
-
   /**
    * get listing of filesystem files location will be data/Py4j/{serviceName}
    * 
@@ -266,7 +275,7 @@ private String getClientKey(Py4JServerConnection gatewayConnection) {
   public List<String> getScriptList() throws IOException {
     List<String> sorted = new ArrayList<>();
     System.out.println(CodecUtils.toJson(config));
-    Py4jConfig c = (Py4jConfig)config;
+    Py4jConfig c = (Py4jConfig) config;
     List<File> files = FileIO.getFileList(c.scriptRootDir, true);
     for (File file : files) {
       if (file.toString().endsWith(".py")) {
@@ -278,13 +287,17 @@ public List<String> getScriptList() throws IOException {
   }
 
   /**
-   * Sink for standard output from Py4j-related subprocesses.
-   * This method immediately publishes the output on {@link #publishStdOut(String)}.
+   * Sink for standard output from Py4j-related subprocesses. This method
+   * immediately publishes the output on {@link #publishStdOut(String)}.
    *
-   * @param msg The output from a py4j related subprocess.
+   * @param msg
+   *          The output from a py4j related subprocess.
    */
   public void handleStdOut(String msg) {
-    invoke("publishStdOut", msg);
+    if (!"\n".equals(msg)) {
+      logs.add(msg);
+      invoke("publishStdOut", msg);
+    }
   }
 
   /**
@@ -296,8 +309,7 @@ public void onPython(String code) {
     log.info("onPython {}", code);
     exec(code);
   }
-  
-  
+
   public String onPythonMessage(Message msg) {
     // create wrapper to tunnel incoming message - include original sender?
     Message tunnelMsg = Message.createMessage(msg.sender, getName(), "onPythonMessage", msg);
@@ -335,7 +347,7 @@ public void openExampleScript(String serviceType) throws IOException {
    * @throws IOException
    */
   public void openScript(String scriptName) throws IOException {
-    Py4jConfig c = (Py4jConfig)config;
+    Py4jConfig c = (Py4jConfig) config;
     File script = new File(c.scriptRootDir + fs + scriptName);
 
     if (!script.exists()) {
@@ -362,12 +374,15 @@ public boolean preProcessHook(Message msg) {
         // back to the Python process, but:
         // 1. its useless for users - no way to access the content ?
         // 2. you can't do anything with it
-        // So, I've chosen to json encode it here, and the Py4j.py MessageHandler will
+        // So, I've chosen to json encode it here, and the Py4j.py
+        // MessageHandler will
         // decode it into a Python dictionary \o/
-        // we do single encoding including the parameter array - there is no header needed
-        // with method and other details, as the invoke here is invoking directly in the
+        // we do single encoding including the parameter array - there is no
+        // header needed
+        // with method and other details, as the invoke here is invoking
+        // directly in the
         // Py4j.py script
-                
+
         String json = CodecUtils.toJson(msg);
         // handler.invoke(msg.method, json);
         log.info(String.format("handler %s", json));
@@ -396,7 +411,7 @@ public String publishStdOut(String data) {
   public void saveScript(String scriptName, String code) throws IOException {
     if (scriptName != null && !scriptName.toLowerCase().endsWith(".py")) {
       scriptName = scriptName + ".py";
-    }    
+    }
     FileIO.toFile(config.scriptRootDir + fs + scriptName, code);
     info("saved file %s", scriptName);
   }
@@ -439,9 +454,10 @@ public void start() {
         info("server started listening on %s:%d", gateway.getAddress(), gateway.getListeningPort());
         handler = (Executor) gateway.getPythonServerEntryPoint(new Class[] { Executor.class });
 
-//        sleep(100);
-//        String[] services = Runtime.getServiceNames();
-//        sendRemote(Message.createMessage(getName(), "runtime", "onServiceNames", services));
+        // sleep(100);
+        // String[] services = Runtime.getServiceNames();
+        // sendRemote(Message.createMessage(getName(), "runtime",
+        // "onServiceNames", services));
 
       } else {
         log.info("Py4j gateway server already started");
@@ -462,7 +478,7 @@ public void startPythonProcess() {
       String pythonScript = new File(getResourceDir() + fs + "Py4j.py").getAbsolutePath();
 
       // Script requires full name as first command line argument
-      String[] pythonArgs = {getFullName()};
+      String[] pythonArgs = { getFullName() };
 
       // Build the command to start the Python process
       ProcessBuilder processBuilder;
@@ -475,8 +491,10 @@ public void startPythonProcess() {
           String python = Loader.load(org.bytedeco.cpython.python.class);
           String venvLib = new File(python).getParent() + fs + "lib" + fs + "venv" + fs + "scripts" + fs + "nt";
           if (Platform.getLocalInstance().isWindows()) {
-            // Super hacky workaround, venv works differently on Windows and requires these two
-            // files, but they are not distributed in bare-bones Python or in any pip packages.
+            // Super hacky workaround, venv works differently on Windows and
+            // requires these two
+            // files, but they are not distributed in bare-bones Python or in
+            // any pip packages.
             // So we copy them where it expects, and it seems to work now
             FileIO.copy(getResourceDir() + fs + "python.exe", venvLib + fs + "python.exe");
             FileIO.copy(getResourceDir() + fs + "pythonw.exe", venvLib + fs + "pythonw.exe");
@@ -515,13 +533,15 @@ public void startPythonProcess() {
   }
 
   /**
-   * Install a list of packages into the environment Py4j is running in.
-   * Py4j does not need to be running/connected to call this method as it
-   * spawns a new subprocess to invoke Pip. Output from pip is echoed
-   * via {@link #handleStdOut(String)}.
+   * Install a list of packages into the environment Py4j is running in. Py4j
+   * does not need to be running/connected to call this method as it spawns a
+   * new subprocess to invoke Pip. Output from pip is echoed via
+   * {@link #handleStdOut(String)}.
    * 
-   * @param packages The list of packages to install. Must be findable by Pip
-   * @throws IOException If an I/O error occurs running Pip.
+   * @param packages
+   *          The list of packages to install. Must be findable by Pip
+   * @throws IOException
+   *           If an I/O error occurs running Pip.
    */
   public void installPipPackages(List<String> packages) throws IOException {
     List<String> commandArgs = new ArrayList<>(List.of("-m", "pip", "install"));
@@ -530,8 +550,7 @@ public void installPipPackages(List<String> packages) throws IOException {
     pipProcess.command().addAll(commandArgs);
     Process proc = pipProcess.redirectErrorStream(true).start();
     new Thread(() -> {
-      BufferedReader stdOutput = new BufferedReader(new
-              InputStreamReader(proc.getInputStream()));
+      BufferedReader stdOutput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
       String s;
       try {
         while ((s = stdOutput.readLine()) != null) {
@@ -614,7 +633,7 @@ public void updateScript(String scriptName, String code) {
       error("cannot find script %s to update", scriptName);
     }
   }
-  
+
   public static void main(String[] args) {
     try {
 
@@ -624,7 +643,8 @@ public static void main(String[] args) {
       webgui.autoStartBrowser(false);
       webgui.startService();
       // Runtime.start("servo", "Servo");
-      Py4j py4j = (Py4j) Runtime.start("py4j", "Py4j");
+      Runtime.start("py4j", "Py4j");
+      // Runtime.start("python", "Python");
 
     } catch (Exception e) {
       log.error("main threw", e);
@@ -634,7 +654,7 @@ public static void main(String[] args) {
   @Override
   public void connect(String uri) throws Exception {
     // host:port of python process running py4j ???
-    
+
   }
 
   /**
@@ -662,6 +682,9 @@ public List<String> getClientIds() {
   public Map<String, Connection> getClients() {
     return Runtime.getInstance().getConnections(getName());
   }
-    
-}
 
+  public void clear() {
+    logs = new SlidingWindowList<>(300);
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/service/Python.java b/src/main/java/org/myrobotlab/service/Python.java
index c3d18162ed..74c7c49fd1 100644
--- a/src/main/java/org/myrobotlab/service/Python.java
+++ b/src/main/java/org/myrobotlab/service/Python.java
@@ -18,11 +18,13 @@
 import org.myrobotlab.framework.interfaces.MessageListener;
 import org.myrobotlab.framework.interfaces.ServiceInterface;
 import org.myrobotlab.framework.repo.ServiceData;
+import org.myrobotlab.generics.SlidingWindowList;
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.io.FindFile;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.Logging;
 import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.config.PythonConfig;
 import org.myrobotlab.service.data.Script;
 import org.myrobotlab.service.interfaces.Processor;
@@ -184,6 +186,12 @@ public void run() {
   private final transient HashMap<String, PyObject> objectCache = new HashMap<String, PyObject>();
 
   private static final long serialVersionUID = 1L;
+  
+  /**
+   * a sliding window of logs
+   */
+  protected List<String> logs = new SlidingWindowList<>(300);
+  
 
   protected int newScriptCnt = 0;
 
@@ -339,6 +347,7 @@ public void closeScript(String file) {
    *          the code to append
    * @return the resulting concatenation
    */
+  @Deprecated /* wtf is this for? */
   public String appendScript(String code) {
     invoke("publishAppend", code);
     return code;
@@ -719,12 +728,14 @@ public String publishAppend(String code) {
     return code;
   }
 
-  public String publishStdOut(String data) {
-    return data;
+  public String publishStdOut(String msg) {
+    logs.add(msg);
+    return msg;
   }
 
-  public String publishStdError(String data) {
-    return data;
+  public String publishStdError(String msg) {
+    logs.add(msg);
+    return msg;
   }
 
   public void setLocalScriptDir(String path) {
@@ -1068,4 +1079,8 @@ public PythonConfig getConfig() {
     return config;
   }
 
+  public void clear() {
+    logs = new SlidingWindowList<>(300);
+  }
+  
 }
diff --git a/src/main/resources/resource/Py4j/Py4j.py b/src/main/resources/resource/Py4j/Py4j.py
index 3c75cd94c5..ecd6271655 100644
--- a/src/main/resources/resource/Py4j/Py4j.py
+++ b/src/main/resources/resource/Py4j/Py4j.py
@@ -1,10 +1,10 @@
 ################################
 # Py4j.py
 # more info here: https://www.py4j.org/
-# Py4J enables Python programs running in a Python interpreter to dynamically access 
-# Java objects in a Java Virtual Machine. 
-# Methods are called as if the Java objects resided in the Python interpreter and 
-# Java collections can be accessed through standard Python collection methods. 
+# Py4J enables Python programs running in a Python interpreter to dynamically access
+# Java objects in a Java Virtual Machine.
+# Methods are called as if the Java objects resided in the Python interpreter and
+# Java collections can be accessed through standard Python collection methods.
 # Py4J also enables Java programs to call back Python objects. Py4J is distributed under the BSD license
 # Python 2.7 -to- 3.x is supported
 # In your python 3.x project
@@ -12,11 +12,12 @@
 # you have full access to mrl instance that's running
 # the gateway
 
-import sys
 import json
+import sys
 from abc import ABC, abstractmethod
-from py4j.java_collections import JavaObject, JavaClass
-from py4j.java_gateway import JavaGateway, CallbackServerParameters, GatewayParameters
+
+from py4j.java_collections import JavaClass, JavaObject
+from py4j.java_gateway import CallbackServerParameters, GatewayParameters, JavaGateway
 
 
 class Service(ABC):
@@ -30,11 +31,11 @@ def __getattr__(self, attr):
     def __str__(self):
         # Delegate string representation to the underlying Java object
         return str(self.java_object)
-    
+
     def subscribe(self, event):
         print("subscribe")
         self.java_object.subscribe(event)
-    
+
     @abstractmethod
     def getType(self):
         pass
@@ -42,8 +43,8 @@ def getType(self):
 
 class NeoPixel(Service):
     def __init__(self, name):
-        super().__init__(name)        
-    
+        super().__init__(name)
+
     def getType(self):
         return "NeoPixel"
 
@@ -54,7 +55,7 @@ def onFlash(self):
 class InMoov2(Service):
     def __init__(self, name):
         super().__init__(name)
-        self.subscribe('onStateChange')
+        self.subscribe("onStateChange")
 
     def getType(self):
         return "InMoov2"
@@ -62,9 +63,9 @@ def getType(self):
     def onOnStateChange(self, state):
         print("onOnStateChange")
         print(state)
-        print(state.get('last'))
-        print(state.get('current'))
-        print(state.get('event'))
+        print(state.get("last"))
+        print(state.get("current"))
+        print(state.get("event"))
 
 
 # TODO dynamically add classes that you don't bother to check in
@@ -77,7 +78,8 @@ def onOnStateChange(self, state):
 # FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!!
 runtime = None
 
-# TODO - rename to mrl_lib ? 
+
+# TODO - rename to mrl_lib ?
 # e.g.
 # mrl = mrl_lib.connect("localhost", 1099)
 # i01 = InMoov("i01", mrl)
@@ -102,9 +104,11 @@ def __init__(self):
         self.stderr = sys.stderr
         sys.stdout = self
         sys.stderr = self
-        self.gateway = JavaGateway(callback_server_parameters=CallbackServerParameters(),
-                                   python_server_entry_point=self,
-                                   gateway_parameters=GatewayParameters(auto_convert=True))
+        self.gateway = JavaGateway(
+            callback_server_parameters=CallbackServerParameters(),
+            python_server_entry_point=self,
+            gateway_parameters=GatewayParameters(auto_convert=True),
+        )
         self.runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance()
         # FIXME - REMOVE THIS - DO NOT SET ANY GLOBALS !!!!
         runtime = self.runtime
@@ -116,33 +120,33 @@ def construct_runtime(self):
         Constructs a new Runtime instance and returns it.
         """
         jvm_runtime = self.gateway.jvm.org.myrobotlab.service.Runtime.getInstance()
-        
+
         # Define class attributes and methods as dictionaries
         class_attributes = {
-            'x': 0,
-            'y': 0,
-            'move': lambda self, dx, dy: setattr(self, 'x', self.x + dx) or setattr(self, 'y', self.y + dy),
-            'get_position': lambda self: (self.x, self.y),
+            "x": 0,
+            "y": 0,
+            "move": lambda self, dx, dy: setattr(self, "x", self.x + dx)
+            or setattr(self, "y", self.y + dy),
+            "get_position": lambda self: (self.x, self.y),
         }
 
         # Create the class dynamically using the type() function
-        MyDynamicClass = type('MyDynamicClass', (object,), class_attributes)
+        MyDynamicClass = type("MyDynamicClass", (object,), class_attributes)
 
         # Create an instance of the dynamically created class
         obj = MyDynamicClass()
 
-
         return self.runtime
 
     # Define the callback function
     def handle_connection_break(self):
         # Add your custom logic here to handle the connection break
-        print("Connection with Java gateway was lost or terminated.")        
+        print("Connection with Java gateway was lost or terminated.")
         print("goodbye.")
         sys.exit(1)
 
-    def write(self,string):
-        if (self.py4j):
+    def write(self, string):
+        if self.py4j:
             self.py4j.handleStdOut(string)
 
     def flush(self):
@@ -167,7 +171,7 @@ def setName(self, name):
         print("reference to runtime")
         # TODO print env vars PYTHONPATH etc
         return name
-    
+
     def getRuntime(self):
         return self.runtime
 
@@ -261,11 +265,13 @@ def convert_array(self, array):
         return result
 
     class Java:
-        implements = ['org.myrobotlab.framework.interfaces.Invoker']
+        implements = ["org.myrobotlab.framework.interfaces.Invoker"]
 
 
 handler = MessageHandler()
 if len(sys.argv) > 1:
     handler.setName(sys.argv[1])
 else:
-    raise RuntimeError("This script requires the full name of the Py4j service as its first command-line argument")
+    raise RuntimeError(
+        "This script requires the full name of the Py4j service as its first command-line argument"
+    )
diff --git a/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js b/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js
index 9e646e47a3..5b5ff5d0e4 100644
--- a/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/Py4jGui.js
@@ -10,7 +10,6 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
 
     // filesystem list of scripts
     $scope.scriptList = []
-    $scope.logs = []
 
     // this UI's currently active script
     $scope.activeKey = null
@@ -33,11 +32,11 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
             $scope.$apply()
             break
         case 'onStdOut':
-            if (data !== "\n"){
-                $scope.logs.unshift(data)
-                if ($scope.logs.length > 100) {
-                    $scope.logs.pop()
-                }                
+            if (data !== "\n") {
+                $scope.service.logs.unshift(data)
+                if ($scope.service.logs.length > 300) {
+                    $scope.service.logs.pop()
+                }
                 $scope.$apply()
             }
             break
@@ -54,7 +53,7 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
             break
         case 'onStatus':
             if (data.level == 'error') {
-                $scope.logs.unshift(data.detail)
+                $scope.service.logs.unshift(data.detail)
             }
             console.info("onStatus ", data)
             $scope.$apply()
@@ -105,11 +104,11 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
             templateUrl: 'addScript.html',
             controller: function($scope, $uibModalInstance) {
                 $scope.ok = function() {
-                    if (!$scope.filename){
+                    if (!$scope.filename) {
                         console.error('filename cannot be null')
                         return
                     }
-                    
+
                     msg.send('addScript', $scope.filename, '# new awesome robot script\n')
                     $uibModalInstance.close($scope.filename)
                 }
@@ -138,47 +137,46 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
     }
 
     $scope.installPackage = function() {
-            var modalInstance = $uibModal.open({
-                templateUrl: 'installPackage.html',
-                controller: function($scope, $uibModalInstance) {
-                    $scope.ok = function() {
-                        if (!$scope.packageName){
-                            console.error('filename cannot be null')
-                            return
-                        }
-
-                        msg.send('installPipPackages', [$scope.packageName])
-                        $uibModalInstance.close($scope.packageName)
+        var modalInstance = $uibModal.open({
+            templateUrl: 'installPackage.html',
+            controller: function($scope, $uibModalInstance) {
+                $scope.ok = function() {
+                    if (!$scope.packageName) {
+                        console.error('filename cannot be null')
+                        return
                     }
 
-                    $scope.cancel = function() {
-                        $uibModalInstance.dismiss('cancel')
-                    }
+                    msg.send('installPipPackages', [$scope.packageName])
+                    $uibModalInstance.close($scope.packageName)
+                }
 
-                    $scope.checkEnterKey = function(event) {
-                        if (event.keyCode === 13) {
-                            $scope.ok()
-                        }
+                $scope.cancel = function() {
+                    $uibModalInstance.dismiss('cancel')
+                }
+
+                $scope.checkEnterKey = function(event) {
+                    if (event.keyCode === 13) {
+                        $scope.ok()
                     }
+                }
 
-                },
-                size: 'sm'
-            })
-
-            modalInstance.result.then(function(filename) {
-                // Do something with the filename
-                console.log("Filename: ", filename)
-            }, function() {
-                // Modal dismissed
-                console.log("Modal dismissed")
-            })
-        }
+            },
+            size: 'sm'
+        })
 
+        modalInstance.result.then(function(filename) {
+            // Do something with the filename
+            console.log("Filename: ", filename)
+        }, function() {
+            // Modal dismissed
+            console.log("Modal dismissed")
+        })
+    }
 
     $scope.openScript = function() {
-        
+
         msg.send('getScriptList')
-        
+
         var modalInstance = $uibModal.open({
             templateUrl: 'openScript.html',
             scope: $scope,
@@ -210,7 +208,10 @@ angular.module('mrlapp.service.Py4jGui', []).controller('Py4jGuiCtrl', ['$scope'
             console.log("Modal dismissed")
         })
     }
-    
+
+    $scope.clear = function() {
+        msg.send('clear')
+    }
 
     msg.subscribe('publishStdOut')
     msg.subscribe('publishAppend')
diff --git a/src/main/resources/resource/WebGui/app/service/js/PythonGui.js b/src/main/resources/resource/WebGui/app/service/js/PythonGui.js
index c893ce5389..a6184f56a3 100644
--- a/src/main/resources/resource/WebGui/app/service/js/PythonGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/PythonGui.js
@@ -14,7 +14,6 @@ angular.module("mrlapp.service.PythonGui", []).controller("PythonGuiCtrl", [
 
     // filesystem list of scripts
     $scope.scriptList = []
-    $scope.log = ""
 
     // this UI's currently active script
     $scope.activeKey = null
@@ -36,25 +35,26 @@ angular.module("mrlapp.service.PythonGui", []).controller("PythonGuiCtrl", [
           _self.updateState(data)
           $scope.$apply()
           break
-        case "onStdOut":
-          $scope.log = data + $scope.log
-          $scope.$apply()
-          break
-        case "onAppend":
-          $scope.log = data + $scope.log
-          $scope.$apply()
-          break
+        case 'onStdOut':
+            if (data !== "\n") {
+                $scope.service.logs.unshift(data)
+                if ($scope.service.logs.length > 300) {
+                    $scope.service.logs.pop()
+                }
+                $scope.$apply()
+            }
+            break
         case "onScriptList":
           $scope.scriptList = data
           $scope.$apply()
           break
         case "onStatus":
-          if (data.level == "error") {
-            $scope.log = data.detail + "\n" + $scope.log
-          }
-          console.info("onStatus ", data)
-          $scope.$apply()
-          break
+            if (data.level == 'error') {
+                $scope.service.logs.unshift(data.detail)
+            }
+            console.info("onStatus ", data)
+            $scope.$apply()
+            break
         default:
           console.error("ERROR - unhandled method " + msg.method)
           break
@@ -133,11 +133,11 @@ angular.module("mrlapp.service.PythonGui", []).controller("PythonGuiCtrl", [
       modalInstance.result.then(
         function (filename) {
           // Do something with the filename
-          console.log("Filename: ", filename)
+          console.info("Filename: ", filename)
         },
         function () {
           // Modal dismissed
-          console.log("Modal dismissed")
+          console.info("Modal dismissed")
         }
       )
     }
@@ -170,17 +170,20 @@ angular.module("mrlapp.service.PythonGui", []).controller("PythonGuiCtrl", [
       modalInstance.result.then(
         function (filename) {
           // Do something with the filename
-          console.log("Filename: ", filename)
+          console.info("Filename: ", filename)
         },
         function () {
           // Modal dismissed
-          console.log("Modal dismissed")
+          console.info("Modal dismissed")
         }
       )
     }
 
+    $scope.clear = function() {
+        msg.send('clear')
+    }
+    
     msg.subscribe("publishStdOut")
-    msg.subscribe("publishAppend")
     msg.subscribe("getClients")
     msg.subscribe("getScriptList")
     msg.send("getScriptList")
diff --git a/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html b/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html
index d13bbce690..6e9e5fab5a 100644
--- a/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/Py4jGui.html
@@ -90,13 +90,19 @@
 <div class="row">
     <span>
         console 
-         <button ng-click="log = '';$apply()" class="btn btn-default">
+         <button ng-click="clear();service.logs = [];$apply();" class="btn btn-default">
             <span class="glyphicon glyphicon-remove"/>
         </button>
     </span>
-    <br/>
-    <pre>{{log}}</pre>
 </div>
+<br/>
+<table>
+    <tbody>
+        <tr ng-repeat="e in service.logs track by $index">
+            <td><small>{{e}}</small></td>
+        </tr>
+    </tbody>
+</table>
 
 <script type="text/ng-template" id="installPackage.html">
     <div class="modal-header">
@@ -147,3 +153,4 @@
       <button class="btn btn-default" ng-click="cancel()">Cancel</button>
     </div>
 </script>
+
diff --git a/src/main/resources/resource/WebGui/app/service/views/PythonGui.html b/src/main/resources/resource/WebGui/app/service/views/PythonGui.html
index fb7f488954..5bfcecee5d 100644
--- a/src/main/resources/resource/WebGui/app/service/views/PythonGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/PythonGui.html
@@ -74,16 +74,26 @@
         </uib-tab>
     </uib-tabset>
 </div>
+
 <div class="row">
     <span>
         console 
-         <button ng-click="log = '';$apply()" class="btn btn-default">
+         <button ng-click="clear();service.logs = [];$apply();" class="btn btn-default">
             <span class="glyphicon glyphicon-remove"/>
         </button>
     </span>
-    <br/>
-    <pre>{{log}}</pre>
 </div>
+
+<br/>
+<table>
+    <tbody>
+        <tr ng-repeat="e in service.logs track by $index">
+            <td><small>{{e}}</small></td>
+        </tr>
+    </tbody>
+</table>
+
+
 <script type="text/ng-template" id="addPythonScript.html">
     <div class="modal-header">
       Enter Filename

From f963e3fd2bfe73d1acdd253e747d55abe1e0dd9a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 11:56:37 -0800
Subject: [PATCH 099/131] test last workflow

---
 .github/{ => workflows}/build.yml | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename .github/{ => workflows}/build.yml (100%)

diff --git a/.github/build.yml b/.github/workflows/build.yml
similarity index 100%
rename from .github/build.yml
rename to .github/workflows/build.yml

From 53591c0f78080e636a6d8c0446d4640d5e6e8998 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 12:00:15 -0800
Subject: [PATCH 100/131] pull_request to push

---
 .github/workflows/build.yml | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b510c17054..10fe55026e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -3,8 +3,8 @@
 name: Java CI
 
 on:
-  # push:
-  pull_request:
+  push:
+  # pull_request:
 
 jobs:
   build:
@@ -84,4 +84,3 @@ jobs:
           tag_name: ${{ steps.version.outputs.version }}
           generate_release_notes: true
           body_path: ./release-template.md
-

From 8376de1e040fca426264951be9162590b135b6e8 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 17:02:11 -0800
Subject: [PATCH 101/131] excluding OpenCV tests from github build

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 10fe55026e..1e61b3bb79 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Dependency Test # installs all dependencies
         run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
       - name: Build with Maven # currently cannot test opencv
-        run: mvn clean verify -q
+        run: mvn clean verify -q -DexcludeTests=*OpenCV*
 
       - name: Get next version
         uses: reecetech/version-increment@2023.9.3

From 7f5e1733244719efcbeebb41dafce341792599e7 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 18:42:05 -0800
Subject: [PATCH 102/131] trying to filter out tests on the command line again

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1e61b3bb79..3129bbd4ba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Dependency Test # installs all dependencies
         run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
       - name: Build with Maven # currently cannot test opencv
-        run: mvn clean verify -q -DexcludeTests=*OpenCV*
+        run: mvn clean verify -q -Dexcludes='**/*OpenCV*'
 
       - name: Get next version
         uses: reecetech/version-increment@2023.9.3

From a80364c6c841acdabeabbf67f0481032cb55e678 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 19:05:15 -0800
Subject: [PATCH 103/131] try filtering at command line again

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3129bbd4ba..716117e803 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Dependency Test # installs all dependencies
         run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q
       - name: Build with Maven # currently cannot test opencv
-        run: mvn clean verify -q -Dexcludes='**/*OpenCV*'
+        run: mvn clean verify -q -Dtest=!org.myrobotlab.opencv.*
 
       - name: Get next version
         uses: reecetech/version-increment@2023.9.3

From 3ebe9b185353e66638a72418189506259ca670e0 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 19:42:08 -0800
Subject: [PATCH 104/131] libgtk2.0-0 added to build

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 716117e803..dba0f2dadc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,7 +23,7 @@ jobs:
           cache: "maven"
 
       - name: Install Missing Dependencies
-        run: sudo apt-get install -y libv4l-0 libopencv-dev python3-opencv
+        run: sudo apt-get install -y libv4l-0 libopencv-dev python3-opencv libgtk2.0-0
 
       - name: Dependency Test # installs all dependencies
         run: mvn test -Dtest=org.myrobotlab.framework.DependencyTest -q

From df72ce359fcc44a09ee5aabcced6a164ac9d2f03 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 28 Feb 2024 21:12:23 -0800
Subject: [PATCH 105/131] java 18

---
 .github/workflows/build.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dba0f2dadc..b512495625 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,11 +13,11 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
-      - name: Set up JDK 11
+      - name: Set up java
         uses: actions/setup-java@v3
 
         with:
-          java-version: "11"
+          java-version: "18"
           distribution: "adopt"
           # NEATO ! CACHE !!!!
           cache: "maven"

From 3b63f256ecb74c122d4576d31490be6d739182f2 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 2 Mar 2024 05:58:20 -0800
Subject: [PATCH 106/131] worky terminal manager

---
 .../org/myrobotlab/process/NodeTerminal.java  |  13 +
 .../myrobotlab/process/PythonTerminal.java    | 108 ++++++
 .../org/myrobotlab/process/Ros2Terminal.java  |  13 +
 .../org/myrobotlab/process/RosTerminal.java   |  13 +
 .../java/org/myrobotlab/process/Terminal.java | 343 ++++++++++++++++++
 src/main/java/org/myrobotlab/service/Log.java |   6 +
 .../myrobotlab/service/TerminalManager.java   | 149 ++++++++
 .../service/config/TerminalManagerConfig.java |  12 +
 .../service/meta/TerminalManagerMeta.java     |  23 ++
 9 files changed, 680 insertions(+)
 create mode 100644 src/main/java/org/myrobotlab/process/NodeTerminal.java
 create mode 100644 src/main/java/org/myrobotlab/process/PythonTerminal.java
 create mode 100644 src/main/java/org/myrobotlab/process/Ros2Terminal.java
 create mode 100644 src/main/java/org/myrobotlab/process/RosTerminal.java
 create mode 100644 src/main/java/org/myrobotlab/process/Terminal.java
 create mode 100644 src/main/java/org/myrobotlab/service/TerminalManager.java
 create mode 100644 src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
 create mode 100644 src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java

diff --git a/src/main/java/org/myrobotlab/process/NodeTerminal.java b/src/main/java/org/myrobotlab/process/NodeTerminal.java
new file mode 100644
index 0000000000..8efb3a6167
--- /dev/null
+++ b/src/main/java/org/myrobotlab/process/NodeTerminal.java
@@ -0,0 +1,13 @@
+package org.myrobotlab.process;
+
+import java.io.IOException;
+
+import org.myrobotlab.service.TerminalManager;
+
+public class NodeTerminal extends Terminal {
+
+  public NodeTerminal(TerminalManager service, String name) throws IOException {
+    super(service, name);
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/process/PythonTerminal.java b/src/main/java/org/myrobotlab/process/PythonTerminal.java
new file mode 100644
index 0000000000..deb7858177
--- /dev/null
+++ b/src/main/java/org/myrobotlab/process/PythonTerminal.java
@@ -0,0 +1,108 @@
+package org.myrobotlab.process;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.myrobotlab.framework.Service;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.TerminalManager;
+import org.myrobotlab.service.Runtime;
+import org.slf4j.Logger;
+
+public class PythonTerminal extends Terminal {
+
+  /**
+   * name of the venv
+   */
+  protected String venvName = "venv";
+
+  public final static Logger log = LoggerFactory.getLogger(PythonTerminal.class);
+
+  public PythonTerminal(TerminalManager service, String name) throws IOException {
+    super(service, name);
+  }
+
+  @Override
+  public String getVersion() {
+    try {
+      processCommand(getScriptCmd("python --version"));
+      Service.sleep(300);
+      return outputCapture.toString();
+    } catch (Exception e) {
+      service.error(e);
+    }
+    return null;
+  }
+
+  public void installPipPackages(List<String> packages) {
+    String packagesString = String.join(" ", packages);
+    String command = "pip install " + packagesString;
+    processCommand(command + "\n");
+  }
+
+  public void installPipPackage(String string) {
+    // TODO Auto-generated method stub
+
+  }
+
+  public void activateVirtualEnv() {
+    if (isWindows()) {
+      processCommand(venvName + "\\Scripts\\activate");
+    } else {
+      // source is "bash"
+      // processCommand("source " + venvName + "/bin/activate");
+      // the posix way
+      processCommand(". " + venvName + "/bin/activate");
+    }
+    Service.sleep(300);
+  }
+
+  public void installVirtualEnv() {
+    installVirtualEnv(venvName);
+  }
+
+  public void installVirtualEnv(String venvName) {
+    this.venvName = venvName;
+    // processCommand(getScriptCmd("python -m venv " + venvName));
+    processCommand("python -m venv " + venvName);
+    Service.sleep(300);
+  }
+
+  public static void main(String[] args) {
+    try {
+      TerminalManager processor = (TerminalManager) Runtime.start("processor", "ManagedProcess");
+      PythonTerminal shell = new PythonTerminal(processor, "python");
+      // shell.setWorkspace(".." + File.separator + "webcam");
+      shell.start(".." + File.separator + "webcam");
+      shell.installVirtualEnv();
+      shell.activateVirtualEnv();
+      // shell.installPipPackage("");
+      shell.installPipPackages(Arrays.asList("aiortc aiohttp"));
+
+      shell.processCommand("python webcam.py");
+      System.out.println(shell.getPids().toString());
+
+      shell.terminate();
+
+      // Example usage
+      String directory = "../webcam";
+      String venvName = "venv";
+      String packageName = "package_name";
+      String pythonScript = "your_script.py";
+
+      // shell.setupAndRunPythonEnvironment(directory, venvName, packageName,
+      // pythonScript);
+
+      // Wait for the completion or handle accordingly
+      // shell.waitForCompletion();
+
+      // Terminate the shell if necessary
+      // shell.terminate();
+
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+  }
+}
diff --git a/src/main/java/org/myrobotlab/process/Ros2Terminal.java b/src/main/java/org/myrobotlab/process/Ros2Terminal.java
new file mode 100644
index 0000000000..d64d0fa53c
--- /dev/null
+++ b/src/main/java/org/myrobotlab/process/Ros2Terminal.java
@@ -0,0 +1,13 @@
+package org.myrobotlab.process;
+
+import java.io.IOException;
+
+import org.myrobotlab.service.TerminalManager;
+
+public class Ros2Terminal extends Terminal {
+
+  public Ros2Terminal(TerminalManager service, String name) throws IOException {
+    super(service, name);
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/process/RosTerminal.java b/src/main/java/org/myrobotlab/process/RosTerminal.java
new file mode 100644
index 0000000000..ea45f0d0ab
--- /dev/null
+++ b/src/main/java/org/myrobotlab/process/RosTerminal.java
@@ -0,0 +1,13 @@
+package org.myrobotlab.process;
+
+import java.io.IOException;
+
+import org.myrobotlab.service.TerminalManager;
+
+public class RosTerminal extends Terminal {
+
+  public RosTerminal(TerminalManager service, String name) throws IOException {
+    super(service, name);
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/process/Terminal.java b/src/main/java/org/myrobotlab/process/Terminal.java
new file mode 100644
index 0000000000..22a1827484
--- /dev/null
+++ b/src/main/java/org/myrobotlab/process/Terminal.java
@@ -0,0 +1,343 @@
+package org.myrobotlab.process;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.TerminalManager;
+import org.myrobotlab.service.Runtime;
+import org.slf4j.Logger;
+
+public class Terminal {
+
+  public final static Logger log = LoggerFactory.getLogger(Terminal.class);
+
+  public boolean isRunning = false;
+
+  /**
+   * executor service for managing streams
+   */
+  private transient ExecutorService executorService;
+
+  /**
+   * lock for synchonizing
+   */
+  protected transient Object lock = new Object();
+
+  /**
+   * name of this shell
+   */
+  protected String name;
+
+  /**
+   * output buffer
+   */
+  protected StringBuilder outputCapture = new StringBuilder();
+
+  /**
+   * The pid of the sub process
+   */
+  protected Long pid;
+
+  /**
+   * list of pids for this shell
+   */
+  protected Set<Long> pids = new HashSet<>();
+
+  /**
+   * process handler
+   */
+  private transient Process process;
+
+  /**
+   * reference to mrl service
+   */
+  protected transient TerminalManager service;
+
+  /**
+   * The initial command that started the shell
+   */
+  protected String shellCommand = null;
+
+  /**
+   * The directory where the interactive shell will do its work, where the
+   * process will start
+   */
+  protected String workspace = ".";
+
+  public Terminal(TerminalManager service, String name) {
+    // can increase to handle more input
+    this.executorService = Executors.newFixedThreadPool(3);
+    this.service = service;
+    this.name = name;
+  }
+
+  public void clearOutput() {
+    outputCapture = new StringBuilder();
+  }
+
+  private String determineShellCommand() {
+    String osName = System.getProperty("os.name").toLowerCase();
+    if (osName.contains("win")) {
+      return "cmd";
+    } else {
+      return "/bin/sh"; // Works for Unix/Linux/Mac
+    }
+  }
+
+  public boolean doesExecutableExist(String name) {
+    return false;
+  }
+
+  /**
+   * <pre>
+   *  FIXME - finish !
+    
+   public void processAndWait(String command) throws IOException {
+     String completionMarker = "Command completed -- unique marker " + System.currentTimeMillis();
+     processCommand(command + "\n");
+     processCommand("echo \"" + completionMarker + "\"\n");
+     
+     StringBuilder commandOutput = new StringBuilder();
+     String line;
+     while ((line = readLineWithTimeout()) != null) { // Implement readLineWithTimeout according to your input handling
+         if (line.contains(completionMarker)) {
+             break;
+         }
+         commandOutput.append(line).append("\n");
+     }
+     // Now commandOutput contains the output from the command, and you know the command finished.
+  }
+   * </pre>
+   */
+
+  public String getCapturedOutput() {
+    synchronized (outputCapture) {
+      return outputCapture.toString();
+    }
+  }
+
+  public Set<Long> getPids() {
+    Set<Long> scanPids = new HashSet<>();
+    if (process.isAlive()) {
+      process.descendants().forEach(processHandle -> {
+        scanPids.add(processHandle.pid());
+      });
+    }
+    pids = scanPids;
+    return pids;
+  }
+
+  /**
+   * cmd for executing a script
+   * 
+   * @param scriptPath
+   * @return
+   */
+  public String getScriptCmd(String scriptPath) {
+    if (isWindows()) {
+      return ("cmd /c \"" + scriptPath + "\"\n");
+    } else {
+      return ("/bin/sh \"" + scriptPath + "\"\n");
+    }
+  }
+
+  public String getTemplate(String templateName) {
+    try {
+      byte[] bytes = Files.readAllBytes(getTemplatePath(templateName));
+      if (bytes != null) {
+        return new String(bytes);
+      }
+    } catch (IOException e) {
+      service.error(e);
+    }
+    return null;
+  }
+
+  // private void startStreamGobbler(InputStream inputStream, String streamName)
+  // {
+  // executorService.submit(() -> {
+  // new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(line
+  // -> {
+  // System.out.println(line); // Print the line
+  // synchronized (outputCapture) {
+  // outputCapture.append(line).append("\n"); // Capture the line
+  // }
+  // });
+  // });
+  // }
+
+  public Path getTemplatePath(String templateName) {
+    Path scriptPath = Paths.get(service.getResourceDir() + File.separator + "templates" + File.separator, templateName + (isWindows() ? ".bat" : ".sh"));
+    return scriptPath;
+  }
+
+  public String getVersion() {
+    return "0.0.0";
+  }
+
+  public boolean isWindows() {
+    return System.getProperty("os.name").toLowerCase().contains("win");
+  }
+
+  public void processCommand(String input) {
+    try {
+      if (input == null) {
+        input = "";
+      }
+      if (process == null) {
+        service.error("cannot process a command when the terminal isn't started");
+        return;
+      }
+      OutputStream outputStream = process.getOutputStream();
+      outputStream.write(String.format("%s\n", input).getBytes());
+      outputStream.flush();
+    } catch (Exception e) {
+      service.error(e);
+    }
+  }
+
+  // New method to process a list of commands
+  public void processCommands(List<String> commands) throws IOException {
+    for (String command : commands) {
+      processCommand(command + "\n");
+    }
+  }
+
+  private void shutdownExecutor() {
+    executorService.shutdownNow();
+  }
+
+  public void start() {
+    start(workspace);
+  }
+
+  /**
+   * Start an interactive shell in a workspace directory
+   * 
+   * @param workspace
+   */
+  public void start(String workspace) {
+    if (!isRunning) {
+      synchronized (lock) {
+        try {
+          shellCommand = determineShellCommand();
+          ProcessBuilder processBuilder = new ProcessBuilder(shellCommand.split(" "));
+          processBuilder.redirectErrorStream(true); // Merge stdout and stderr
+
+          if (workspace != null && !workspace.isEmpty()) {
+            this.workspace = workspace;
+            processBuilder.directory(new File(workspace)); // Set the CWD for
+                                                           // the
+                                                           // process
+          }
+
+          process = processBuilder.start();
+          pid = process.pid();
+          isRunning = true;
+
+          startStreamGobbler(process.getInputStream(), "OUTPUT");
+          // FIXME option to attach to stdIn
+          // should
+          // startUserInputForwarder();
+        } catch (Exception e) {
+          isRunning = false;
+          service.error(e);
+        }
+        service.broadcastState();
+      }
+    } else {
+      log.info("{} already started", name);
+    }
+  }
+
+  private void startStreamGobbler(InputStream inputStream, String streamName) {
+    executorService.submit(() -> {
+      try {
+        byte[] buffer = new byte[1024]; // Adjust size as needed
+        int length;
+        while ((length = inputStream.read(buffer)) != -1) {
+          String text = new String(buffer, 0, length);
+          // Synchronize writing to the outputCapture to ensure thread safety
+          synchronized (outputCapture) {
+            System.out.print(text); // Print the text as it comes without
+                                    // waiting for a new line
+            outputCapture.append(text); // Append the text to the output capture
+          }
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    });
+  }
+
+  private void startUserInputForwarder() {
+    executorService.submit(() -> {
+      try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
+        String inputLine;
+        while ((inputLine = reader.readLine()) != null) {
+          processCommand(inputLine);
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    });
+  }
+
+  public void terminate() throws IOException {
+    synchronized (lock) {
+      // Optionally send a quit command to the shell if it supports graceful
+      // exit.
+      // Example for Unix/Linux/Mac: sendInput("exit\n");
+      // For Windows, it might be different or not necessary.
+      if (process != null) {
+        process.descendants().forEach(processHandle -> {
+          log.info("Terminating PID: " + processHandle.pid());
+          processHandle.destroyForcibly(); // Attempts to terminate the process
+        });
+        // destroying parent
+        process.destroyForcibly();
+        process = null;
+        shutdownExecutor(); // Shutdown the executor service
+      }
+      isRunning = false;
+    }
+    service.broadcastState();
+  }
+
+  public int waitForCompletion() throws InterruptedException {
+    process.waitFor();
+    shutdownExecutor();
+    return process.exitValue();
+  }
+
+  public static void main(String[] args) {
+    try {
+      TerminalManager processor = (TerminalManager) Runtime.start("processor", "ManagedProcess");
+      Terminal shell = new Terminal(processor, "basic tty");
+      shell.start();
+      // Example usage of the new method if you want to process a list of
+      // commands
+      List<String> commands = Arrays.asList("echo Hello", "ls");
+      shell.processCommands(commands);
+      int exitCode = shell.waitForCompletion();
+      System.out.println("Shell exited with code: " + exitCode);
+    } catch (IOException | InterruptedException e) {
+      e.printStackTrace();
+    }
+  }
+
+}
diff --git a/src/main/java/org/myrobotlab/service/Log.java b/src/main/java/org/myrobotlab/service/Log.java
index 51e388a994..f0043af0ea 100644
--- a/src/main/java/org/myrobotlab/service/Log.java
+++ b/src/main/java/org/myrobotlab/service/Log.java
@@ -54,6 +54,7 @@ public static class LogEntry {
     public String threadName;
     public String className;
     public String body;
+    public String src;
 
     public LogEntry(ILoggingEvent event) {
       ts = event.getTimeStamp();
@@ -63,6 +64,11 @@ public LogEntry(ILoggingEvent event) {
       body = event.getFormattedMessage();
     }
 
+    public LogEntry() {
+      ts = System.currentTimeMillis();
+      threadName = Thread.currentThread().getName();
+    }
+
     @Override
     public String toString() {
       return String.format("%d %s %s %s %s", ts, level, threadName, className, body);
diff --git a/src/main/java/org/myrobotlab/service/TerminalManager.java b/src/main/java/org/myrobotlab/service/TerminalManager.java
new file mode 100644
index 0000000000..d44d362e35
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/TerminalManager.java
@@ -0,0 +1,149 @@
+package org.myrobotlab.service;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import org.myrobotlab.framework.Instantiator;
+import org.myrobotlab.framework.Service;
+import org.myrobotlab.logging.Level;
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.logging.LoggingFactory;
+import org.myrobotlab.process.Terminal;
+import org.myrobotlab.service.Log.LogEntry;
+import org.myrobotlab.service.config.TerminalManagerConfig;
+import org.slf4j.Logger;
+
+public class TerminalManager extends Service<TerminalManagerConfig> {
+
+  public static class TerminalStartupConfig {
+    public String type = null; // Python Node Ros
+
+  }
+
+  public final static Logger log = LoggerFactory.getLogger(TerminalManager.class);
+
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * Thread safe map of all the terminals
+   */
+  protected Map<String, Terminal> terminals = new ConcurrentSkipListMap<>();
+
+  public TerminalManager(String n, String id) {
+    super(n, id);
+  }
+
+  /**
+   * Process a command against a named terminal
+   * 
+   * @param name
+   * @param command
+   */
+  public void processCommand(String name, String command) {
+    if (!terminals.containsKey(name)) {
+      error("could not find terminal %s to process command %s", name, command);
+      return;
+    }
+    Terminal terminal = terminals.get(name);
+    terminal.processCommand(command);
+  }
+
+  /**
+   * Start a generalized simple terminal
+   * 
+   * @param name
+   *          terminal name
+   */
+  public void startTerminal(String name) {
+    startTerminal(name, null);
+  }
+
+  /**
+   * Terminates the terminal
+   * 
+   * @param name
+   *          terminal name
+   */
+  public void terminateTerminal(String name) {
+    log.info("terminating terminal {}", name);
+    if (terminals.containsKey(name)) {
+      try {
+        Terminal terminal = terminals.get(name);
+        terminal.terminate();
+      } catch (Exception e) {
+        error(e);
+      }
+    } else {
+      info("%s terminal does not exist", name);
+    }
+  }
+
+  /**
+   * Save configuration of the terminal including if its currently running
+   * 
+   * @param name
+   *          terminal name
+   */
+  public void saveTerminal(String name) {
+    log.info("saving terminal {}", name);
+    // TODO - get terminal startup info and
+    // save it to config
+  }
+
+  public void deleteTerminal(String name) {
+    log.info("deleting terminal {}", name);
+    if (terminals.containsKey(name)) {
+      terminals.remove(name);
+    } else {
+      info("%s terminal does not exist", name);
+    }
+  }
+
+  public LogEntry publishStdOut(String name, String msg) {
+    LogEntry entry = new LogEntry();
+    entry.src = name;
+    entry.level = "INFO";
+    entry.className = this.getClass().getCanonicalName();
+    entry.body = msg;
+    return entry;
+  }
+
+  public void startTerminal(String name, String type) {
+    log.info("starting terminal {} {}", name, type);
+
+    Terminal terminal = null;
+    String fullType = null;
+
+    if (type == null) {
+      type = "";
+    }
+
+    if (!type.contains(".")) {
+      fullType = "org.myrobotlab.process." + type + "Terminal";
+    } else {
+      fullType = type;
+    }
+
+    if (terminals.containsKey(name)) {
+      terminal = terminals.get(name);
+    } else {
+      terminal = (Terminal) Instantiator.getNewInstance(fullType, this, name);
+      terminals.put(name, terminal);
+    }
+    terminal.start();
+  }
+
+  public static void main(String[] args) {
+    try {
+
+      LoggingFactory.init(Level.INFO);
+
+      TerminalManager manager = (TerminalManager) Runtime.start("manager", "TerminalManager");
+      Runtime.start("webgui", "WebGui");
+      manager.startTerminal("basic");
+
+    } catch (Exception e) {
+      log.error("main threw", e);
+    }
+  }
+}
diff --git a/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java b/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
new file mode 100644
index 0000000000..c4e39c6dcd
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
@@ -0,0 +1,12 @@
+package org.myrobotlab.service.config;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.myrobotlab.service.TerminalManager.TerminalStartupConfig;
+
+public class TerminalManagerConfig extends ServiceConfig {
+
+  Map<String, TerminalStartupConfig> terminals = new TreeMap<>();
+
+}
diff --git a/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java b/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java
new file mode 100644
index 0000000000..28f448218c
--- /dev/null
+++ b/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java
@@ -0,0 +1,23 @@
+package org.myrobotlab.service.meta;
+
+import org.myrobotlab.logging.LoggerFactory;
+import org.myrobotlab.service.meta.abstracts.MetaData;
+import org.slf4j.Logger;
+
+public class TerminalManagerMeta extends MetaData {
+  private static final long serialVersionUID = 1L;
+  public final static Logger log = LoggerFactory.getLogger(TerminalManagerMeta.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 TerminalManagerMeta() {
+
+    addDescription("Service that can manage subprocesses");
+    addCategory("programming", "service");
+    setAvailable(true);
+
+  }
+
+}

From ea1a1d462f9f2ff2d1c8fbb4fb6a3326f885f9d8 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 2 Mar 2024 05:59:40 -0800
Subject: [PATCH 107/131] added 2 more

---
 .../app/service/js/TerminalManagerGui.js      | 60 +++++++++++++++++++
 .../app/service/views/TerminalManagerGui.html | 29 +++++++++
 2 files changed, 89 insertions(+)
 create mode 100644 src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
 create mode 100644 src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html

diff --git a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
new file mode 100644
index 0000000000..fbf30ccd8d
--- /dev/null
+++ b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
@@ -0,0 +1,60 @@
+angular.module("mrlapp.service.TerminalManagerGui", []).controller("TerminalManagerGuiCtrl", [
+  "$scope",
+  "mrl",
+  function ($scope, mrl) {
+    console.info("TerminalManagerGuiCtrl")
+    var _self = this
+    var msg = this.msg
+
+    // GOOD TEMPLATE TO FOLLOW
+    this.updateState = function (service) {
+      $scope.service = service
+    }
+
+    // init scope variables
+    $scope.onTime = null
+    $scope.onEpoch = null
+
+    this.onMsg = function (inMsg) {
+      let data = inMsg.data[0]
+      switch (inMsg.method) {
+        case "onState":
+          _self.updateState(data)
+          $scope.$apply()
+          break
+        case "onTime":
+          const date = new Date(data)
+          $scope.onTime = date.toLocaleString()
+          $scope.$apply()
+          break
+        case "onEpoch":
+          $scope.onEpoch = data
+          $scope.$apply()
+          break
+        default:
+          console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
+          break
+      }
+    }
+
+    // Assuming `service` is your service managing terminals
+    $scope.startTerminal = function (key) {
+      msg.send("startTerminal", key)
+    }
+
+    $scope.terminateTerminal = function (key) {
+      msg.send("terminateTerminal", key)
+    }
+
+    $scope.saveTerminal = function (key) {
+      msg.send("saveTerminal", key)
+    }
+
+    $scope.deleteTerminal = function (key) {
+      msg.send("deleteTerminal", key)
+    }
+
+    msg.subscribe("publishEpoch")
+    msg.subscribe(this)
+  },
+])
diff --git a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
new file mode 100644
index 0000000000..acfa8daaab
--- /dev/null
+++ b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
@@ -0,0 +1,29 @@
+<table class="table table-striped">
+  <thead>
+    <tr>
+      <th style="width: 10%"></th>
+      <th style="width: 10%"></th>
+      <th style="width: 10%"></th>
+      <th style="width: 10%">Name</th>
+      <th style="width: 10%">PID</th>
+      <th style="width: 10%">Shell</th>
+      <th style="width: 70%">Command</th>
+      <th style="width: 70%">Control</th>
+    </tr>
+  </thead>
+  <tr ng-repeat="(key, value) in service.terminals">
+    <td style="width: 10%"><input type="radio" name="selectedTerminal" ng-model="ctrl.selectedTerminal" ng-value="terminal" /></td>
+    <td style="width: 10%"><img src="TerminalManager.png" width="16" /></td>
+    <td style="width: 10%"><img ng-src="{{value.connected ? 'connected.png' : 'disconnected.png'}}" alt="Connection Status" width="16" /></td>
+    <td style="width: 10%"><small>{{key}}</small></td>
+    <td><small>{{value.pid}}</small></td>
+    <td><small>{{value.shellCommand}}</small></td>
+    <td><small>{{value.lastInput}}</small></td>
+    <td>
+      <button ng-click="startTerminal(key)">Start</button>
+      <button ng-click="terminateTerminal(key)">Terminate</button>
+      <button ng-click="saveTerminal(key)">Save</button>
+      <button ng-click="deleteTerminal(key)">Delete</button>
+    </td>
+  </tr>
+</table>

From 11457e61152e662496231ed0d7e9736d8251ac69 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 2 Mar 2024 08:30:22 -0800
Subject: [PATCH 108/131] updated openblas 1.5.8

---
 .../java/org/myrobotlab/service/meta/Deeplearning4jMeta.java  | 2 +-
 src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java     | 1 +
 .../java/org/myrobotlab/service/meta/TesseractOcrMeta.java    | 4 +++-
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
index 23fc4aeff8..1387f42739 100644
--- a/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/Deeplearning4jMeta.java
@@ -26,7 +26,7 @@ public Deeplearning4jMeta() {
     addDependency("org.bytedeco", "javacpp", "1.5.8");
 
     // REMOVED FOR COLLISION
-    // addDependency("org.bytedeco", "openblas", "0.3.17-" + "1.5.6");
+    addDependency("org.bytedeco", "openblas", "0.3.21-" + "1.5.8");
 
     // dl4j deps.
     addDependency("org.deeplearning4j", "deeplearning4j-core", dl4jVersion);
diff --git a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
index 73d284cdb6..03428446dd 100644
--- a/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/OpenCVMeta.java
@@ -20,6 +20,7 @@ public OpenCVMeta() {
     // addDependency("org.bytedeco", "javacv", javaCvVersion);
     addDependency("org.bytedeco", "javacv-platform", javaCvVersion);
     addDependency("org.bytedeco", "javacpp", javaCvVersion);
+    addDependency("org.bytedeco", "openblas", "0.3.21-" + javaCvVersion);
     // FIXME - finish with cmdLine flag -gpu vs cudaEnabled for DL4J ?
     boolean gpu = false;
     if (gpu) {
diff --git a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
index 8c8d9a2554..e8149447f7 100644
--- a/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
+++ b/src/main/java/org/myrobotlab/service/meta/TesseractOcrMeta.java
@@ -13,13 +13,15 @@ public class TesseractOcrMeta extends MetaData {
    * dependencies, and all other meta data related to the service.
    */
   public TesseractOcrMeta() {
+    String javaCvVersion = "1.5.8";
 
-    String tesseractVersion = "5.2.0-1.5.8";
+    String tesseractVersion = "5.2.0-" + javaCvVersion;
     addDescription("Optical character recognition - the ability to read");
     addCategory("ai", "vision");
     addDependency("org.bytedeco", "tesseract", tesseractVersion);
     addDependency("org.bytedeco", "tesseract-platform", tesseractVersion);
     addDependency("tesseract", "tessdata", "0.0.2", "zip");
+    addDependency("org.bytedeco", "openblas", "0.3.21-" + javaCvVersion);
 
   }
 

From a36eba5715acf621a0bf81bef9c811e8e8430000 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 2 Mar 2024 08:36:24 -0800
Subject: [PATCH 109/131] forgot img

---
 src/main/resources/resource/TerminalManager.png | Bin 0 -> 1937 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/main/resources/resource/TerminalManager.png

diff --git a/src/main/resources/resource/TerminalManager.png b/src/main/resources/resource/TerminalManager.png
new file mode 100644
index 0000000000000000000000000000000000000000..e212bb9b2d00a7208a1173f2514387b218bee6de
GIT binary patch
literal 1937
zcmV;C2X6R@P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf2PR2GK~!i%otQa`
zRoxPXSGV(kqNs_nU~FjUKQI;yG*B=T%#B2IH8n61L{OXpQN%G2LopKs13^@rXZ!1C
zU7vm5uHNU|&-+4k)v9?-d+$Co_wV1Io0*wuBIUWcxj)i>fNe~dsP=!0%&k0D52StO
z*|TSJt5>g{T3F|llY`Wa;os)?Tg=?Od$;d^NfI@=vW3B<*lC!Q*PPSx>921;LS352
z=FOYivSrH_B(2CIV+E_d=9!ciXftjs9=v}2y8Za^qsue*?%kW4ot<s#)~##%_wR2{
zpFSNX(H-VFb`$<BvXL6Upt*TDTg9e5`P;W|+qG-g+Qy9=+pk~0{(FShuV3Hx?c3Kr
zefrdvELpOkn8|9wX~4(E$Nbg({{6dik-o}u^3C7bcJ=Djwqe7DK7m;sFj`ROrAwD~
zVnXaf1-@A%3D8`Q;po&)YTg(rb@r)l=qep)mc}_}%a<>2Jf&8wSkaFbHWLVwPa<H`
zV3G~W$yFVc{$Q3t1;$OAHnj^EF0}32xA*bNl`GrYwQJj|Rjc~s+$|^zOw*~iMvnCd
z+N7$>+Oz!OEI~Q~R)5>JZSBmNGwuBO^KI9zT|JN;aS~o`V8SuV$$)9ij@dFk3H^eD
z?aaXY_wU>8-MicE+qc`+ty|mCqet7VTesStJ$rid`J^HdFt!#1IhN87WFB>iF<svQ
z4RDgU8*kpcX~&Ko>rHc<&>uW_u$?=1u6_CPWtafS9F?bBCzk}O-LV`{mtGk62>=ur
zG67?VN9)U%FWb9!@47F?NS{i?Lm+dhPhU{S(-uWA#t+^nU_qggN$uUcw_Ut=u^l>e
zsBPJ@rCqvosq47Z#yQc1MKk|2AN`Wlr;l(v>;SL;Ixgz`ORgUuF7)Zsr~5@de*Abp
zS`^^@z+I5?osrEVTTrSE0%Ph#0lL&1H|zie9P`}g=o~q6r0v|fvz<MAwx4?@1Cb0L
z-Vr=r*Q{C7$@c|z8?@PJ&{x>3{QLLsy&qt1@(!2SVC7e@UbP)NcC?!}Z+1P+fBg8d
zbB-31z>gJY<$X6`V*=;}dW!;i!batQ8Z^xl@$1*GJ-0B&aD?ClyBh@4h%=}0Z{NQ4
zBqZdi^!V}P_VD4uHvdK29z1x^9zA;0o;-OnT=Hxtn?|2DfsVSLKY#8I44WSFB_FhC
z2LKcJ`SWLQo~B7kB98(S<1$~re!X3}a;1MA@_}{v^5u5p#*H334Ox%o$jbt;Ff>p6
z*e8oH9-5hNV)z<8eE4vC@!~~qa1y+@1m_ElF_UD~&@*`&u~5RM@qv)lr*D<9vokCf
zb%`6@fjUkQL}Mfa$PzXt=#FI${2}z7YjzhOZ3*>>+7sj9bJ|q^H6ivSnqcKvZML3m
zhcnKV2j)Wm;lqdCJn?zSGq>a974#|%m)JttIKZAZ#t^|YBDo95=ETRd&E(7lQkIK4
zHeUsP{d3x8Kssj;;5dDZ7ud<gO7qy|IH9X~%ptG#G3W!g=V1rNFqRaK9<J;YgB|dg
zP@u*;mZvUdVGW_|g5lpMP*<_S<XONm<oV_<*mxEOV>*DY628Fv0DXcw=obA;IR=Yo
zazNvpZ_as7_Gc&b%Q!HJ1M?aK;OQe=Y;400=vWg78-iSME@s+Hj0}jc@>tG9+7mUF
zT2Q~l7&O)b0+|b0pD2=JkogknBhqh@JPnI}E6Ii&dpUE(6VnPa*8hLSX|Fjz^@WBV
z;BizRqnu}KJPNs-APdxH#F*ZK$`#c%Hc=?+(_5cF4W@BG(=^|m{^<0{MC!oUSY%Vx
z58GJI>XUl<0(}A!;QNGg&atuaG2maDs$)<a8}ox~2xy06tI!wQf(g(x=R6zFVu?w~
zg>m}mU*l^2v?tFPpsshSu_jB@Bvr1BRT;{K(Jr@k#;F_YYskrVAjm~FB%hQhIY2hP
z$`Hu>6_fPBUZG(JpxR&^>zBYs0@}?7DUZbh^b$}HQdebYmd7VE@?vCg1-48COe8Ws
z^}u*g<3Pp&?CCF%-GxO@p_D<&fN>_B)Q~oH$<r^Pt@>1dn)lt+?*LC9cK}$p20E^K
z;bcVmSDUCd<Q!)$_4W#1aESp<lM0R{);Gt>iPUGFvDh3>s>yd5yiWjP7f|RV#&i|L
z*QOG+iD~(2o-toPd-@Ub600PrCUKm}st=gd*rYh14UR3x73%C4KI{O*cRs}D1aIUV
zLn)7qCsF#5cgzM)TOF%&0uYm!Id$q(|6z+CW$)a%)1E(nzThX1{}*E`__wGz=#$U-
z0|yQalbJbr@?`hoHP3IFa2(z0u|~83R>Dt&ziAp@>nIyPCa2DSVY~jsi4$|II1V%e
z7Y2$PkJ&)#66p`ue-g&>*D(t>+yTxL%H+Vy0-+z19an#3DSdt7=`-eQ52;&h|JD8h
X?w=+{ykB|700000NkvXXu0mjf3k<|N

literal 0
HcmV?d00001


From 45d7f8b81e4d18fdee0045fc641bf8d0bdd79bac Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Wed, 6 Mar 2024 07:48:34 -0800
Subject: [PATCH 110/131] workyish tm

---
 .../myrobotlab/process/PythonTerminal.java    |   4 +-
 .../java/org/myrobotlab/process/Terminal.java | 147 ++++++++++-----
 .../myrobotlab/service/TerminalManager.java   | 169 +++++++++++++-----
 .../app/service/js/TerminalManagerGui.js      |  79 ++++----
 .../WebGui/app/service/tab-header.html        |   4 +-
 .../app/service/views/TerminalManagerGui.html | 124 ++++++++++---
 6 files changed, 369 insertions(+), 158 deletions(-)

diff --git a/src/main/java/org/myrobotlab/process/PythonTerminal.java b/src/main/java/org/myrobotlab/process/PythonTerminal.java
index deb7858177..42be1648fd 100644
--- a/src/main/java/org/myrobotlab/process/PythonTerminal.java
+++ b/src/main/java/org/myrobotlab/process/PythonTerminal.java
@@ -27,9 +27,7 @@ public PythonTerminal(TerminalManager service, String name) throws IOException {
   @Override
   public String getVersion() {
     try {
-      processCommand(getScriptCmd("python --version"));
-      Service.sleep(300);
-      return outputCapture.toString();
+      return processBlockingCommand(getScriptCmd("python --version"));
     } catch (Exception e) {
       service.error(e);
     }
diff --git a/src/main/java/org/myrobotlab/process/Terminal.java b/src/main/java/org/myrobotlab/process/Terminal.java
index 22a1827484..3b5000543b 100644
--- a/src/main/java/org/myrobotlab/process/Terminal.java
+++ b/src/main/java/org/myrobotlab/process/Terminal.java
@@ -9,16 +9,18 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
+import org.myrobotlab.generics.SlidingWindowList;
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.TerminalManager;
-import org.myrobotlab.service.Runtime;
 import org.slf4j.Logger;
 
 public class Terminal {
@@ -27,6 +29,8 @@ public class Terminal {
 
   public boolean isRunning = false;
 
+  protected final String BOUNDARY_MARKER = "----------------terminal-cmd-boundary-7MA4YWxkTrZu0gW----------------";
+
   /**
    * executor service for managing streams
    */
@@ -45,7 +49,8 @@ public class Terminal {
   /**
    * output buffer
    */
-  protected StringBuilder outputCapture = new StringBuilder();
+  // protected List<String> output  = new SlidingWindowList<>(300);
+  protected StringBuilder output = new StringBuilder();  
 
   /**
    * The pid of the sub process
@@ -72,12 +77,31 @@ public class Terminal {
    */
   protected String shellCommand = null;
 
+  /**
+   * For synchronous output
+   */
+  private transient BlockingQueue<String> blockingOutputQueue = new LinkedBlockingQueue<>();
+
   /**
    * The directory where the interactive shell will do its work, where the
    * process will start
    */
   protected String workspace = ".";
 
+  /**
+   * last command processed
+   */
+  protected String lastCmd = null;
+  
+  
+  public static class TerminalCmd {
+    public long ts = System.currentTimeMillis();
+    public String src;
+    public String terminal;
+    public String cmd;
+  }
+  
+
   public Terminal(TerminalManager service, String name) {
     // can increase to handle more input
     this.executorService = Executors.newFixedThreadPool(3);
@@ -86,7 +110,8 @@ public Terminal(TerminalManager service, String name) {
   }
 
   public void clearOutput() {
-    outputCapture = new StringBuilder();
+    // output = new SlidingWindowList<>(300);
+    output = new StringBuilder();
   }
 
   private String determineShellCommand() {
@@ -94,7 +119,15 @@ private String determineShellCommand() {
     if (osName.contains("win")) {
       return "cmd";
     } else {
-      return "/bin/sh"; // Works for Unix/Linux/Mac
+      // return "/bin/sh"; // Works for Unix/Linux/Mac
+      String bashPath = "/bin/bash";
+      File bashFile = new File(bashPath);
+      if (bashFile.exists()) {
+          return bashPath;
+      } else {
+          // Fallback to sh if Bash is not found (less ideal)
+          return "/bin/sh";
+      }
     }
   }
 
@@ -124,12 +157,6 @@ public void processAndWait(String command) throws IOException {
    * </pre>
    */
 
-  public String getCapturedOutput() {
-    synchronized (outputCapture) {
-      return outputCapture.toString();
-    }
-  }
-
   public Set<Long> getPids() {
     Set<Long> scanPids = new HashSet<>();
     if (process.isAlive()) {
@@ -194,19 +221,55 @@ public boolean isWindows() {
   }
 
   public void processCommand(String input) {
-    try {
-      if (input == null) {
-        input = "";
+    processCommand(input, false);
+  }
+
+  public void processCommand(String input, boolean addBoundary) {
+    synchronized (lock) {
+      try {
+        if (input == null) {
+          input = "";
+        }
+        if (process == null) {
+          service.error("cannot process a command when the terminal isn't started");
+          return;
+        }
+        String cmd = null;
+        if (addBoundary) {
+          // windows/mac echo vs linux
+          cmd = String.format("%s\necho %s\n", input, BOUNDARY_MARKER);
+        } else {
+          cmd = String.format("%s\n", input);
+        }
+        lastCmd = cmd;
+        TerminalCmd terminalCmd = new TerminalCmd();
+        terminalCmd.src = service.getName();
+        terminalCmd.terminal = name;
+        terminalCmd.cmd = cmd;
+        service.invoke("publishCmd", terminalCmd);
+        OutputStream outputStream = process.getOutputStream();
+        outputStream.write(cmd.getBytes());
+        outputStream.flush();
+      } catch (Exception e) {
+        service.error(e);
       }
-      if (process == null) {
-        service.error("cannot process a command when the terminal isn't started");
-        return;
+    }
+  }
+
+  // FIXME - should be synchronized with
+  public String processBlockingCommand(String input) {
+    synchronized (lock) {
+      blockingOutputQueue.clear();
+      processCommand(input, true);
+      String ret = null;
+      try {
+        while (isRunning && ret == null) {
+          ret = blockingOutputQueue.poll(100, TimeUnit.MILLISECONDS);
+        }
+      } catch (InterruptedException e) {
+        service.error(e);
       }
-      OutputStream outputStream = process.getOutputStream();
-      outputStream.write(String.format("%s\n", input).getBytes());
-      outputStream.flush();
-    } catch (Exception e) {
-      service.error(e);
+      return ret;
     }
   }
 
@@ -267,19 +330,29 @@ public void start(String workspace) {
   private void startStreamGobbler(InputStream inputStream, String streamName) {
     executorService.submit(() -> {
       try {
-        byte[] buffer = new byte[1024]; // Adjust size as needed
+        byte[] buffer = new byte[8192]; // Adjust size as needed
         int length;
+        StringBuilder dynamicBuffer = new StringBuilder();
         while ((length = inputStream.read(buffer)) != -1) {
           String text = new String(buffer, 0, length);
-          // Synchronize writing to the outputCapture to ensure thread safety
-          synchronized (outputCapture) {
-            System.out.print(text); // Print the text as it comes without
-                                    // waiting for a new line
-            outputCapture.append(text); // Append the text to the output capture
+          // asynchronous publishing of all stdout
+          service.invoke("publishLog", name, text);
+          service.invoke("publishStdOut", text);
+          output.append(text);
+          dynamicBuffer.append(text);
+          System.out.print(text);
+          if (dynamicBuffer.toString().contains(BOUNDARY_MARKER)) {
+            // Boundary marker found, handle command completion here
+            System.out.println("Command execution completed.");
+            // Remove the boundary marker from the output buffer
+            int index = dynamicBuffer.indexOf(BOUNDARY_MARKER);
+            dynamicBuffer.delete(index, index + BOUNDARY_MARKER.length());
+            blockingOutputQueue.add(dynamicBuffer.toString());
+            dynamicBuffer = new StringBuilder();
           }
         }
       } catch (IOException e) {
-        e.printStackTrace();
+        service.error(e);
       }
     });
   }
@@ -324,20 +397,4 @@ public int waitForCompletion() throws InterruptedException {
     return process.exitValue();
   }
 
-  public static void main(String[] args) {
-    try {
-      TerminalManager processor = (TerminalManager) Runtime.start("processor", "ManagedProcess");
-      Terminal shell = new Terminal(processor, "basic tty");
-      shell.start();
-      // Example usage of the new method if you want to process a list of
-      // commands
-      List<String> commands = Arrays.asList("echo Hello", "ls");
-      shell.processCommands(commands);
-      int exitCode = shell.waitForCompletion();
-      System.out.println("Shell exited with code: " + exitCode);
-    } catch (IOException | InterruptedException e) {
-      e.printStackTrace();
-    }
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/TerminalManager.java b/src/main/java/org/myrobotlab/service/TerminalManager.java
index d44d362e35..69af4548b2 100644
--- a/src/main/java/org/myrobotlab/service/TerminalManager.java
+++ b/src/main/java/org/myrobotlab/service/TerminalManager.java
@@ -9,12 +9,21 @@
 import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.process.Terminal;
-import org.myrobotlab.service.Log.LogEntry;
+import org.myrobotlab.process.Terminal.TerminalCmd;
 import org.myrobotlab.service.config.TerminalManagerConfig;
 import org.slf4j.Logger;
 
 public class TerminalManager extends Service<TerminalManagerConfig> {
 
+  public class TerminalLogEntry {
+    public String msg = null;
+    public String src = null;
+    // FIXME - STDERR at some point
+    public String stream = "stdout";
+    public String terminal = null;
+    public long ts = System.currentTimeMillis();
+  }
+
   public static class TerminalStartupConfig {
     public String type = null; // Python Node Ros
 
@@ -33,49 +42,97 @@ public TerminalManager(String n, String id) {
     super(n, id);
   }
 
+  public void deleteTerminal(String name) {
+    log.info("deleting terminal {}", name);
+    if (terminals.containsKey(name)) {
+      terminals.remove(name);
+    } else {
+      info("%s terminal does not exist", name);
+    }
+  }
+
+  /**
+   * Process blocking command in default terminal
+   * 
+   * @param cmd
+   * @return
+   */
+  public String processBlockingCommand(String cmd) {
+    return processBlockingCommand("default", cmd);
+  }
+  
+  /**
+   * Publishes the current command from a terminal
+   * @param cmd
+   * @return
+   */
+  public TerminalCmd publishCmd(TerminalCmd cmd) {
+    return cmd;
+  }
+
+  /**
+   * Synchronously process a command in the terminal
+   * 
+   * @param name
+   * @param cmd
+   */
+  public String processBlockingCommand(String name, String cmd) {
+    if (!terminals.containsKey(name)) {
+      error("could not find terminal %s to process command %s", name, cmd);
+      return null;
+    }
+    Terminal terminal = terminals.get(name);
+    return terminal.processBlockingCommand(cmd);
+  }
+
+  /**
+   * Asynchronously process command in default terminal
+   * 
+   * @param cmd
+   */
+  public void processCommand(String cmd) {
+    processCommand("default", cmd);
+  }
+
   /**
    * Process a command against a named terminal
    * 
    * @param name
-   * @param command
+   * @param cmd
    */
-  public void processCommand(String name, String command) {
+  public void processCommand(String name, String cmd) {
     if (!terminals.containsKey(name)) {
-      error("could not find terminal %s to process command %s", name, command);
+      error("could not find terminal %s to process command %s", name, cmd);
       return;
     }
     Terminal terminal = terminals.get(name);
-    terminal.processCommand(command);
+    terminal.processCommand(cmd);
   }
 
   /**
-   * Start a generalized simple terminal
+   * Structured log publishing
    * 
    * @param name
-   *          terminal name
+   * @param msg
+   * @return
    */
-  public void startTerminal(String name) {
-    startTerminal(name, null);
+  public TerminalLogEntry publishLog(String name, String msg) {
+    TerminalLogEntry entry = new TerminalLogEntry();
+    entry.src = getName();
+    entry.terminal = name;
+    entry.msg = msg;
+    entry.stream = "stdout";
+    return entry;
   }
 
   /**
-   * Terminates the terminal
+   * All stdout/stderr from all terminals is published here
    * 
-   * @param name
-   *          terminal name
+   * @param msg
+   * @return
    */
-  public void terminateTerminal(String name) {
-    log.info("terminating terminal {}", name);
-    if (terminals.containsKey(name)) {
-      try {
-        Terminal terminal = terminals.get(name);
-        terminal.terminate();
-      } catch (Exception e) {
-        error(e);
-      }
-    } else {
-      info("%s terminal does not exist", name);
-    }
+  public String publishStdOut(String msg) {
+    return msg;
   }
 
   /**
@@ -90,25 +147,21 @@ public void saveTerminal(String name) {
     // save it to config
   }
 
-  public void deleteTerminal(String name) {
-    log.info("deleting terminal {}", name);
-    if (terminals.containsKey(name)) {
-      terminals.remove(name);
-    } else {
-      info("%s terminal does not exist", name);
-    }
+  public void startTerminal() {
+    startTerminal("default");
   }
 
-  public LogEntry publishStdOut(String name, String msg) {
-    LogEntry entry = new LogEntry();
-    entry.src = name;
-    entry.level = "INFO";
-    entry.className = this.getClass().getCanonicalName();
-    entry.body = msg;
-    return entry;
+  /**
+   * Start a generalized simple terminal
+   * 
+   * @param name
+   *          terminal name
+   */
+  public void startTerminal(String name) {
+    startTerminal(name, null, null);
   }
 
-  public void startTerminal(String name, String type) {
+  public void startTerminal(String name, String workspace, String type) {
     log.info("starting terminal {} {}", name, type);
 
     Terminal terminal = null;
@@ -118,6 +171,10 @@ public void startTerminal(String name, String type) {
       type = "";
     }
 
+    if (workspace == null) {
+      workspace = ".";
+    }
+
     if (!type.contains(".")) {
       fullType = "org.myrobotlab.process." + type + "Terminal";
     } else {
@@ -130,7 +187,27 @@ public void startTerminal(String name, String type) {
       terminal = (Terminal) Instantiator.getNewInstance(fullType, this, name);
       terminals.put(name, terminal);
     }
-    terminal.start();
+    terminal.start(workspace);
+  }
+
+  /**
+   * Terminates the terminal
+   * 
+   * @param name
+   *          terminal name
+   */
+  public void terminateTerminal(String name) {
+    log.info("terminating terminal {}", name);
+    if (terminals.containsKey(name)) {
+      try {
+        Terminal terminal = terminals.get(name);
+        terminal.terminate();
+      } catch (Exception e) {
+        error(e);
+      }
+    } else {
+      info("%s terminal does not exist", name);
+    }
   }
 
   public static void main(String[] args) {
@@ -140,10 +217,20 @@ public static void main(String[] args) {
 
       TerminalManager manager = (TerminalManager) Runtime.start("manager", "TerminalManager");
       Runtime.start("webgui", "WebGui");
-      manager.startTerminal("basic");
+      manager.startTerminal();
+
+//      for (int i = 0; i < 100; ++i) {
+//        String ls = manager.processBlockingCommand("ls");
+//        manager.processCommand("ls");
+//      }
+      
+//      List<String> commands = Arrays.asList("echo Hello", "ls");
+//      manager.processCommands(commands);
+
 
     } catch (Exception e) {
       log.error("main threw", e);
     }
   }
+
 }
diff --git a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
index fbf30ccd8d..e7f3480f81 100644
--- a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
@@ -1,60 +1,59 @@
-angular.module("mrlapp.service.TerminalManagerGui", []).controller("TerminalManagerGuiCtrl", [
-  "$scope",
-  "mrl",
-  function ($scope, mrl) {
+angular.module("mrlapp.service.TerminalManagerGui", []).controller("TerminalManagerGuiCtrl", ["$scope", "mrl", function($scope, mrl) {
     console.info("TerminalManagerGuiCtrl")
     var _self = this
     var msg = this.msg
 
-    // GOOD TEMPLATE TO FOLLOW
-    this.updateState = function (service) {
-      $scope.service = service
+    $scope.processCommand = function(key, input) {
+        msg.send("processCommand", key, input)
+        $scope.service.inputValue = ""
     }
 
-    // init scope variables
-    $scope.onTime = null
-    $scope.onEpoch = null
-
-    this.onMsg = function (inMsg) {
-      let data = inMsg.data[0]
-      switch (inMsg.method) {
+    this.onMsg = function(inMsg) {
+        let data = inMsg.data[0]
+        switch (inMsg.method) {
         case "onState":
-          _self.updateState(data)
-          $scope.$apply()
-          break
-        case "onTime":
-          const date = new Date(data)
-          $scope.onTime = date.toLocaleString()
-          $scope.$apply()
-          break
-        case "onEpoch":
-          $scope.onEpoch = data
-          $scope.$apply()
-          break
+            $scope.service = data
+            $scope.$apply()
+            break
+        case "onLog":
+            $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output + data.msg
+            let length = $scope.service.terminals[data.terminal].output.length
+            if (length > 1024) {
+                let overLength = length - 1024;
+                $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output.substring(overLength);
+            }
+            $scope.$apply()
+            break
+        case "onStdOut":
+            break
+        case "onCmd":
+            break
         default:
-          console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
-          break
-      }
+            console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
+            break
+        }
     }
 
     // Assuming `service` is your service managing terminals
-    $scope.startTerminal = function (key) {
-      msg.send("startTerminal", key)
+    $scope.startTerminal = function(key) {
+        msg.send("startTerminal", key)
     }
 
-    $scope.terminateTerminal = function (key) {
-      msg.send("terminateTerminal", key)
+    $scope.terminateTerminal = function(key) {
+        msg.send("terminateTerminal", key)
     }
 
-    $scope.saveTerminal = function (key) {
-      msg.send("saveTerminal", key)
+    $scope.saveTerminal = function(key) {
+        msg.send("saveTerminal", key)
     }
 
-    $scope.deleteTerminal = function (key) {
-      msg.send("deleteTerminal", key)
+    $scope.deleteTerminal = function(key) {
+        msg.send("deleteTerminal", key)
     }
 
-    msg.subscribe("publishEpoch")
+    msg.subscribe("publishLog")
+    // msg.subscribe("publishStdOut")
+    msg.subscribe("publishCmd")
     msg.subscribe(this)
-  },
-])
+}
+, ])
diff --git a/src/main/resources/resource/WebGui/app/service/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html
index 8e8448e23d..792051d6bf 100644
--- a/src/main/resources/resource/WebGui/app/service/tab-header.html
+++ b/src/main/resources/resource/WebGui/app/service/tab-header.html
@@ -10,9 +10,7 @@
         <div class="dropdown">
           <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
             <img ng-src="{{::service.simpleName}}.png" alt="" width="16" />
-            <<<<<<< HEAD &nbsp;&nbsp {{::service.simpleName}}&nbsp;&nbsp;{{::service.name}}@{{::service.id}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{::service.serviceVersion}} =======
-            &nbsp;&nbsp {{::service.simpleName}}&nbsp;&nbsp;{{::service.name}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{::service.serviceVersion}} >>>>>>>
-            5a2c9e63ebe81f301c16e7627b72f72c73edb1f6
+            &nbsp;&nbsp {{::service.simpleName}}&nbsp;&nbsp;{{::service.name}}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{::service.serviceVersion}}
             <span class="caret"></span>
           </button>
           <ul class="dropdown-menu">
diff --git a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
index acfa8daaab..3475cb610d 100644
--- a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
@@ -1,29 +1,101 @@
+<style>
+    .inline-buttons {
+        display: inline-block;
+        /* Or display: inline; */
+    }
+
+    .terminal2 {
+        background-color: black;
+        color: #33ff33;
+        font-family: 'Courier New', Courier, monospace;
+        font-size: 0.8em;
+        border-radius: 0;
+        margin: 0;
+        /* Removes default margin */
+        padding: 10px;
+        /* Adjust based on your design needs */
+        border: none;
+        /* Removes border */
+        overflow: auto;
+    }
+
+    .terminal-wrapper {
+          max-height: 800px; /* Adjust based on your needs */
+  overflow-y: auto;
+        display: flex;
+        flex-direction: column;
+        align-items: stretch;
+        /* Ensures child elements fill the container */
+    }
+
+    .terminal-input {
+        background-color: black;
+        color: #33ff33;
+        border: none;
+        /* Removes border */
+        outline: none;
+        /* Removes focus outline */
+        font-family: 'Courier New', Courier, monospace;
+        font-size: 0.8em;
+        padding: 10px;
+        /* Should match the <pre> padding for alignment */
+        width: 100%;
+        /* Ensures it takes up all available width */
+        box-sizing: border-box;
+        /* Includes padding in the width calculation */
+        margin: 0;
+        /* Removes default margin */
+    }
+</style>
 <table class="table table-striped">
-  <thead>
-    <tr>
-      <th style="width: 10%"></th>
-      <th style="width: 10%"></th>
-      <th style="width: 10%"></th>
-      <th style="width: 10%">Name</th>
-      <th style="width: 10%">PID</th>
-      <th style="width: 10%">Shell</th>
-      <th style="width: 70%">Command</th>
-      <th style="width: 70%">Control</th>
+    <thead>
+        <tr>
+            <th></th>
+            <th></th>
+            <th></th>
+            <th>Name</th>
+            <th>PID</th>
+            <th>Shell</th>
+            <th>Command</th>
+            <th>Control</th>
+        </tr>
+    </thead>
+    <tr ng-repeat="(key, value) in service.terminals">
+        <td>
+            <input type="radio" name="selectedTerminal" ng-model="ctrl.selectedTerminal" ng-value="terminal"/>
+        </td>
+        <td>
+            <img src="TerminalManager.png" width="16"/>
+        </td>
+        <td>
+            <img ng-src="{{value.isRunning ? 'connected.png' : 'disconnected.png'}}" alt="Connection Status" width="16"/>
+        </td>
+        <td>
+            <small>{{key}}</small>
+        </td>
+        <td>
+            <small>{{value.pid}}</small>
+        </td>
+        <td>
+            <small>{{value.shellCommand}}</small>
+        </td>
+        <td>
+            <small>{{value.lastInput}}</small>
+        </td>
+        <td>
+            <span class="inline-buttons">
+                <button class="btn btn-sm" ng-click="startTerminal(key)">Start</button>
+                <button class="btn btn-sm" ng-click="terminateTerminal(key)">Terminate</button>
+                <button class="btn btn-sm" ng-click="saveTerminal(key)">Save</button>
+                <button class="btn btn-sm" ng-click="deleteTerminal(key)">Delete</button>
+            </span>
+        </td>
     </tr>
-  </thead>
-  <tr ng-repeat="(key, value) in service.terminals">
-    <td style="width: 10%"><input type="radio" name="selectedTerminal" ng-model="ctrl.selectedTerminal" ng-value="terminal" /></td>
-    <td style="width: 10%"><img src="TerminalManager.png" width="16" /></td>
-    <td style="width: 10%"><img ng-src="{{value.connected ? 'connected.png' : 'disconnected.png'}}" alt="Connection Status" width="16" /></td>
-    <td style="width: 10%"><small>{{key}}</small></td>
-    <td><small>{{value.pid}}</small></td>
-    <td><small>{{value.shellCommand}}</small></td>
-    <td><small>{{value.lastInput}}</small></td>
-    <td>
-      <button ng-click="startTerminal(key)">Start</button>
-      <button ng-click="terminateTerminal(key)">Terminate</button>
-      <button ng-click="saveTerminal(key)">Save</button>
-      <button ng-click="deleteTerminal(key)">Delete</button>
-    </td>
-  </tr>
 </table>
+<div class="terminal-wrapper" ng-repeat="(key, value) in service.terminals">
+    {{key}}<button class="btn btn-sm" ng-click="deleteTerminal(key)">Clear</button>
+    
+    <pre class="terminal2">{{value.output}}</pre>
+    
+    <input class="terminal-input" type="text" class="form-control" id="inputField" ng-model="service.inputValue" ng-keyup="$event.keyCode == 13 ? processCommand(key, service.inputValue) : null" placeholder="type here...">
+</div>

From 107c7422a81d1d2aec34e679f7bf2bcf5142fc7a Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 9 Mar 2024 07:24:20 -0800
Subject: [PATCH 111/131] bottom scroll

---
 .../WebGui/app/service/js/TerminalManagerGui.js   | 15 ++++++++++++++-
 .../app/service/views/TerminalManagerGui.html     |  9 +++++----
 2 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
index e7f3480f81..17a85e6e53 100644
--- a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
@@ -22,11 +22,24 @@ angular.module("mrlapp.service.TerminalManagerGui", []).controller("TerminalMana
                 let overLength = length - 1024;
                 $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output.substring(overLength);
             }
-            $scope.$apply()
+            // $scope.$apply()
+            $scope.$apply(function() {
+                    // Scroll logic here
+                    // Assuming you can uniquely identify the <pre> for this terminal
+                    let terminalElement = document.querySelector('.terminal-wrapper[data-terminal-id="' + data.terminal + '"] .terminal2');
+                    if (terminalElement) {
+                        terminalElement.scrollTop = terminalElement.scrollHeight;
+                    }
+                });
+                
             break
         case "onStdOut":
             break
         case "onCmd":
+            // FIXME - keep a list of commands ... can support history and maybe more importantly 
+            // script generation to make automated packages
+            $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output + '# ' + data.cmd    
+            $scope.$apply()
             break
         default:
             console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
diff --git a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
index 3475cb610d..fe42096ccb 100644
--- a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
@@ -92,10 +92,11 @@
         </td>
     </tr>
 </table>
-<div class="terminal-wrapper" ng-repeat="(key, value) in service.terminals">
+
+<div class="terminal-wrapper" ng-repeat="(key, value) in service.terminals" data-terminal-id="{{key}}">
     {{key}}<button class="btn btn-sm" ng-click="deleteTerminal(key)">Clear</button>
     
-    <pre class="terminal2">{{value.output}}</pre>
+    <pre class="terminal2" id="terminal-{{key}}" >{{value.output}}</pre>
     
-    <input class="terminal-input" type="text" class="form-control" id="inputField" ng-model="service.inputValue" ng-keyup="$event.keyCode == 13 ? processCommand(key, service.inputValue) : null" placeholder="type here...">
-</div>
+    <input class="terminal-input" type="text" class="form-control" ng-model="service.inputValue" ng-keyup="$event.keyCode == 13 ? processCommand(key, service.inputValue) : null" placeholder="type here...">
+</div>
\ No newline at end of file

From 0e5a9c3ec7bc7c09202ef5c9212cc67bd3fa2e45 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Sat, 9 Mar 2024 08:03:49 -0800
Subject: [PATCH 112/131] locked in InMoov2.speakBlocking

---
 .../org/myrobotlab/service/InMoov2Test.java   | 42 ++++++++++++-------
 1 file changed, 28 insertions(+), 14 deletions(-)

diff --git a/src/test/java/org/myrobotlab/service/InMoov2Test.java b/src/test/java/org/myrobotlab/service/InMoov2Test.java
index c9ae337b69..377d00a28a 100644
--- a/src/test/java/org/myrobotlab/service/InMoov2Test.java
+++ b/src/test/java/org/myrobotlab/service/InMoov2Test.java
@@ -1,29 +1,43 @@
 package org.myrobotlab.service;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 import org.myrobotlab.framework.StaticType;
 import org.myrobotlab.service.config.OpenCVConfig;
+import org.myrobotlab.test.AbstractTest;
 
-public class InMoov2Test {
+public class InMoov2Test extends AbstractTest {
 
   @Test
   public void testCvFilters() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
 
-    InMoov2 i01 = (InMoov2)Runtime.start("i01", "InMoov2");
-      
-      // flip
-      i01.setPeerConfigValue("opencv", "flip", true);
-      OpenCVConfig cvconfig = i01.getPeerConfig("opencv", new StaticType<>() {});
-      assertTrue(cvconfig.flip);
-
-      i01.setPeerConfigValue("opencv", "flip", false);
-      cvconfig = i01.getPeerConfig("opencv", new StaticType<>() {});
-      assertFalse(cvconfig.flip);
-      
+    Runtime.setConfig("InMoov2Test");
+
+    InMoov2 i01 = (InMoov2) Runtime.start("i01", "InMoov2");
+    
+
+    // flip
+    i01.setPeerConfigValue("opencv", "flip", true);
+    OpenCVConfig cvconfig = i01.getPeerConfig("opencv", new StaticType<>() {
+    });
+    assertTrue(cvconfig.flip);
+
+    i01.setPeerConfigValue("opencv", "flip", false);
+    cvconfig = i01.getPeerConfig("opencv", new StaticType<>() {
+    });
+    assertFalse(cvconfig.flip);
+    
+    i01.startPeer("mouth");
+
+    long start = System.currentTimeMillis();
+    // i01.setSpeechType("LocalSpeech");
+    i01.speakBlocking(
+        "Hello this is a way to test if speech is actually blocking, if it blocks it should take a little time to say this, if it doesn't work it will execute the next line immediately.");
+    System.out.println(String.format("speech blocking time taken %d", System.currentTimeMillis() - start));
+    assertTrue(start > 2000);
+
   }
 
-  
 }
-

From d3917588b5c4979f57dd99997fdb60ea2767bbe4 Mon Sep 17 00:00:00 2001
From: grog <grog@myrobotlab.org>
Date: Tue, 12 Mar 2024 20:29:50 -0700
Subject: [PATCH 113/131] javadoc fixed

---
 src/main/java/org/myrobotlab/config/ConfigUtils.java | 2 +-
 src/main/java/org/myrobotlab/service/Runtime.java    | 7 +++----
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/myrobotlab/config/ConfigUtils.java b/src/main/java/org/myrobotlab/config/ConfigUtils.java
index 7b793452ac..c8191320be 100644
--- a/src/main/java/org/myrobotlab/config/ConfigUtils.java
+++ b/src/main/java/org/myrobotlab/config/ConfigUtils.java
@@ -46,7 +46,7 @@ public static String getResourceRoot() {
    * data/config/{configName}/runtime.yml If one does exits, it is returned, if
    * one does not exist a default one is created and saved.
    * 
-   * @param configName
+   * @param options
    * @return
    */
   static public RuntimeConfig loadRuntimeConfig(CmdOptions options) {
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 3a4ebda900..9b09766aa9 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -5033,10 +5033,9 @@ public boolean isProcessingConfig() {
    * Sets the directory for the current config. This will be under configRoot +
    * fs + configName. Static wrapper around setConfigName - so it can be used in
    * the same way as all the other common static service methods
-   *
-   * @param configName
-   *          - config dir name under data/config/{config}
-   * @return configName
+   * 
+   * @param name - config dir name under data/config/{config}
+   * @return config dir name
    */
   public static String setConfig(String name) {
     if (name == null) {

From a94466b0967465629bd0ae827d42a0d9a768449c Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Thu, 21 Mar 2024 12:53:49 -0700
Subject: [PATCH 114/131] publish on change current session and synchronized

---
 src/main/java/org/myrobotlab/programab/Session.java | 4 ++--
 src/main/java/org/myrobotlab/service/ProgramAB.java | 7 +++++++
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/programab/Session.java b/src/main/java/org/myrobotlab/programab/Session.java
index a76fb94bec..d7205ba2e4 100644
--- a/src/main/java/org/myrobotlab/programab/Session.java
+++ b/src/main/java/org/myrobotlab/programab/Session.java
@@ -95,7 +95,7 @@ private synchronized Chat getChat() {
     return chat;
   }
 
-  public void savePredicates() {
+  synchronized public void savePredicates() {
     StringBuilder sb = new StringBuilder();
     TreeSet<String> sort = new TreeSet<>();
     sort.addAll(getChat().predicates.keySet());
@@ -122,7 +122,7 @@ public void savePredicates() {
    * Get all current predicate names and values
    * @return
    */
-  public Map<String, String> getPredicates() {
+  synchronized public Map<String, String> getPredicates() {
     TreeMap<String, String> sort = new TreeMap<>();
     // copy keys, making this sort thread safe
     Set<String> keys = new HashSet(getChat().predicates.keySet());
diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java
index c32a0ab437..4d8b547004 100644
--- a/src/main/java/org/myrobotlab/service/ProgramAB.java
+++ b/src/main/java/org/myrobotlab/service/ProgramAB.java
@@ -271,8 +271,15 @@ public Response getResponse(String userName, String botName, String text, boolea
     // update the current session if we want to change which bot is at
     // attention.
     if (updateCurrentSession) {
+      
+      boolean sessionChanged =  (!userName.equals(config.currentUserName) || !botName.equals(config.currentBotName));
+      
       setCurrentUserName(userName);
       setCurrentBotName(botName);
+      
+      if (sessionChanged) {
+        invoke("publishSession", getSessionKey(userName, botName));
+      }
     }
 
     // Get the actual bots aiml based response for this session

From ac4edd47d1a5db4eb865b4060c8189cf60eac9cd Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Fri, 22 Mar 2024 12:14:22 -0700
Subject: [PATCH 115/131] java 11

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b512495625..06d98292ca 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,7 +17,7 @@ jobs:
         uses: actions/setup-java@v3
 
         with:
-          java-version: "18"
+          java-version: "11"
           distribution: "adopt"
           # NEATO ! CACHE !!!!
           cache: "maven"

From f09bf0eb3af2f4173ec37940b338d9417b3652f3 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Tue, 23 Apr 2024 05:59:45 -0700
Subject: [PATCH 116/131] removal of terminal stuff removed

---
 TODO.md                                       |   6 +
 .../org/myrobotlab/process/NodeTerminal.java  |  13 -
 .../myrobotlab/process/PythonTerminal.java    | 106 -----
 .../org/myrobotlab/process/Ros2Terminal.java  |  13 -
 .../org/myrobotlab/process/RosTerminal.java   |  13 -
 .../java/org/myrobotlab/process/Terminal.java | 400 ------------------
 .../java/org/myrobotlab/service/InMoov2.java  |  16 +-
 .../org/myrobotlab/service/InMoov2Arm.java    |  51 ++-
 .../org/myrobotlab/service/InMoov2Hand.java   |  14 +
 .../org/myrobotlab/service/InMoov2Head.java   |   6 +
 .../org/myrobotlab/service/InMoov2Torso.java  |  22 +-
 .../myrobotlab/service/TerminalManager.java   | 236 -----------
 .../java/org/myrobotlab/service/WebGui.java   |   2 +-
 .../service/config/AudioFileConfig.java       |   3 +
 .../service/config/InMoov2Config.java         |  12 +-
 .../service/config/ProgramABConfig.java       |   8 +-
 .../service/config/RemoteSpeechConfig.java    |   5 +-
 .../service/config/RuntimeConfig.java         |   2 +-
 .../service/config/TerminalManagerConfig.java |  12 -
 .../service/meta/TerminalManagerMeta.java     |  23 -
 20 files changed, 94 insertions(+), 869 deletions(-)
 create mode 100644 TODO.md
 delete mode 100644 src/main/java/org/myrobotlab/process/NodeTerminal.java
 delete mode 100644 src/main/java/org/myrobotlab/process/PythonTerminal.java
 delete mode 100644 src/main/java/org/myrobotlab/process/Ros2Terminal.java
 delete mode 100644 src/main/java/org/myrobotlab/process/RosTerminal.java
 delete mode 100644 src/main/java/org/myrobotlab/process/Terminal.java
 delete mode 100644 src/main/java/org/myrobotlab/service/TerminalManager.java
 delete mode 100644 src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
 delete mode 100644 src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java

diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000000..93df2377f6
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,6 @@
+## TODO
+
+- current config name doesn't show up in runtime
+- initCheckUp.py isn't getting run
+- peak is not working or implemented in the UI
+- peak isn't default
diff --git a/src/main/java/org/myrobotlab/process/NodeTerminal.java b/src/main/java/org/myrobotlab/process/NodeTerminal.java
deleted file mode 100644
index 8efb3a6167..0000000000
--- a/src/main/java/org/myrobotlab/process/NodeTerminal.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.myrobotlab.process;
-
-import java.io.IOException;
-
-import org.myrobotlab.service.TerminalManager;
-
-public class NodeTerminal extends Terminal {
-
-  public NodeTerminal(TerminalManager service, String name) throws IOException {
-    super(service, name);
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/process/PythonTerminal.java b/src/main/java/org/myrobotlab/process/PythonTerminal.java
deleted file mode 100644
index 42be1648fd..0000000000
--- a/src/main/java/org/myrobotlab/process/PythonTerminal.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.myrobotlab.process;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
-import org.myrobotlab.framework.Service;
-import org.myrobotlab.logging.LoggerFactory;
-import org.myrobotlab.service.TerminalManager;
-import org.myrobotlab.service.Runtime;
-import org.slf4j.Logger;
-
-public class PythonTerminal extends Terminal {
-
-  /**
-   * name of the venv
-   */
-  protected String venvName = "venv";
-
-  public final static Logger log = LoggerFactory.getLogger(PythonTerminal.class);
-
-  public PythonTerminal(TerminalManager service, String name) throws IOException {
-    super(service, name);
-  }
-
-  @Override
-  public String getVersion() {
-    try {
-      return processBlockingCommand(getScriptCmd("python --version"));
-    } catch (Exception e) {
-      service.error(e);
-    }
-    return null;
-  }
-
-  public void installPipPackages(List<String> packages) {
-    String packagesString = String.join(" ", packages);
-    String command = "pip install " + packagesString;
-    processCommand(command + "\n");
-  }
-
-  public void installPipPackage(String string) {
-    // TODO Auto-generated method stub
-
-  }
-
-  public void activateVirtualEnv() {
-    if (isWindows()) {
-      processCommand(venvName + "\\Scripts\\activate");
-    } else {
-      // source is "bash"
-      // processCommand("source " + venvName + "/bin/activate");
-      // the posix way
-      processCommand(". " + venvName + "/bin/activate");
-    }
-    Service.sleep(300);
-  }
-
-  public void installVirtualEnv() {
-    installVirtualEnv(venvName);
-  }
-
-  public void installVirtualEnv(String venvName) {
-    this.venvName = venvName;
-    // processCommand(getScriptCmd("python -m venv " + venvName));
-    processCommand("python -m venv " + venvName);
-    Service.sleep(300);
-  }
-
-  public static void main(String[] args) {
-    try {
-      TerminalManager processor = (TerminalManager) Runtime.start("processor", "ManagedProcess");
-      PythonTerminal shell = new PythonTerminal(processor, "python");
-      // shell.setWorkspace(".." + File.separator + "webcam");
-      shell.start(".." + File.separator + "webcam");
-      shell.installVirtualEnv();
-      shell.activateVirtualEnv();
-      // shell.installPipPackage("");
-      shell.installPipPackages(Arrays.asList("aiortc aiohttp"));
-
-      shell.processCommand("python webcam.py");
-      System.out.println(shell.getPids().toString());
-
-      shell.terminate();
-
-      // Example usage
-      String directory = "../webcam";
-      String venvName = "venv";
-      String packageName = "package_name";
-      String pythonScript = "your_script.py";
-
-      // shell.setupAndRunPythonEnvironment(directory, venvName, packageName,
-      // pythonScript);
-
-      // Wait for the completion or handle accordingly
-      // shell.waitForCompletion();
-
-      // Terminate the shell if necessary
-      // shell.terminate();
-
-    } catch (IOException e) {
-      e.printStackTrace();
-    }
-  }
-}
diff --git a/src/main/java/org/myrobotlab/process/Ros2Terminal.java b/src/main/java/org/myrobotlab/process/Ros2Terminal.java
deleted file mode 100644
index d64d0fa53c..0000000000
--- a/src/main/java/org/myrobotlab/process/Ros2Terminal.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.myrobotlab.process;
-
-import java.io.IOException;
-
-import org.myrobotlab.service.TerminalManager;
-
-public class Ros2Terminal extends Terminal {
-
-  public Ros2Terminal(TerminalManager service, String name) throws IOException {
-    super(service, name);
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/process/RosTerminal.java b/src/main/java/org/myrobotlab/process/RosTerminal.java
deleted file mode 100644
index ea45f0d0ab..0000000000
--- a/src/main/java/org/myrobotlab/process/RosTerminal.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.myrobotlab.process;
-
-import java.io.IOException;
-
-import org.myrobotlab.service.TerminalManager;
-
-public class RosTerminal extends Terminal {
-
-  public RosTerminal(TerminalManager service, String name) throws IOException {
-    super(service, name);
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/process/Terminal.java b/src/main/java/org/myrobotlab/process/Terminal.java
deleted file mode 100644
index 3b5000543b..0000000000
--- a/src/main/java/org/myrobotlab/process/Terminal.java
+++ /dev/null
@@ -1,400 +0,0 @@
-package org.myrobotlab.process;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-import org.myrobotlab.generics.SlidingWindowList;
-import org.myrobotlab.logging.LoggerFactory;
-import org.myrobotlab.service.TerminalManager;
-import org.slf4j.Logger;
-
-public class Terminal {
-
-  public final static Logger log = LoggerFactory.getLogger(Terminal.class);
-
-  public boolean isRunning = false;
-
-  protected final String BOUNDARY_MARKER = "----------------terminal-cmd-boundary-7MA4YWxkTrZu0gW----------------";
-
-  /**
-   * executor service for managing streams
-   */
-  private transient ExecutorService executorService;
-
-  /**
-   * lock for synchonizing
-   */
-  protected transient Object lock = new Object();
-
-  /**
-   * name of this shell
-   */
-  protected String name;
-
-  /**
-   * output buffer
-   */
-  // protected List<String> output  = new SlidingWindowList<>(300);
-  protected StringBuilder output = new StringBuilder();  
-
-  /**
-   * The pid of the sub process
-   */
-  protected Long pid;
-
-  /**
-   * list of pids for this shell
-   */
-  protected Set<Long> pids = new HashSet<>();
-
-  /**
-   * process handler
-   */
-  private transient Process process;
-
-  /**
-   * reference to mrl service
-   */
-  protected transient TerminalManager service;
-
-  /**
-   * The initial command that started the shell
-   */
-  protected String shellCommand = null;
-
-  /**
-   * For synchronous output
-   */
-  private transient BlockingQueue<String> blockingOutputQueue = new LinkedBlockingQueue<>();
-
-  /**
-   * The directory where the interactive shell will do its work, where the
-   * process will start
-   */
-  protected String workspace = ".";
-
-  /**
-   * last command processed
-   */
-  protected String lastCmd = null;
-  
-  
-  public static class TerminalCmd {
-    public long ts = System.currentTimeMillis();
-    public String src;
-    public String terminal;
-    public String cmd;
-  }
-  
-
-  public Terminal(TerminalManager service, String name) {
-    // can increase to handle more input
-    this.executorService = Executors.newFixedThreadPool(3);
-    this.service = service;
-    this.name = name;
-  }
-
-  public void clearOutput() {
-    // output = new SlidingWindowList<>(300);
-    output = new StringBuilder();
-  }
-
-  private String determineShellCommand() {
-    String osName = System.getProperty("os.name").toLowerCase();
-    if (osName.contains("win")) {
-      return "cmd";
-    } else {
-      // return "/bin/sh"; // Works for Unix/Linux/Mac
-      String bashPath = "/bin/bash";
-      File bashFile = new File(bashPath);
-      if (bashFile.exists()) {
-          return bashPath;
-      } else {
-          // Fallback to sh if Bash is not found (less ideal)
-          return "/bin/sh";
-      }
-    }
-  }
-
-  public boolean doesExecutableExist(String name) {
-    return false;
-  }
-
-  /**
-   * <pre>
-   *  FIXME - finish !
-    
-   public void processAndWait(String command) throws IOException {
-     String completionMarker = "Command completed -- unique marker " + System.currentTimeMillis();
-     processCommand(command + "\n");
-     processCommand("echo \"" + completionMarker + "\"\n");
-     
-     StringBuilder commandOutput = new StringBuilder();
-     String line;
-     while ((line = readLineWithTimeout()) != null) { // Implement readLineWithTimeout according to your input handling
-         if (line.contains(completionMarker)) {
-             break;
-         }
-         commandOutput.append(line).append("\n");
-     }
-     // Now commandOutput contains the output from the command, and you know the command finished.
-  }
-   * </pre>
-   */
-
-  public Set<Long> getPids() {
-    Set<Long> scanPids = new HashSet<>();
-    if (process.isAlive()) {
-      process.descendants().forEach(processHandle -> {
-        scanPids.add(processHandle.pid());
-      });
-    }
-    pids = scanPids;
-    return pids;
-  }
-
-  /**
-   * cmd for executing a script
-   * 
-   * @param scriptPath
-   * @return
-   */
-  public String getScriptCmd(String scriptPath) {
-    if (isWindows()) {
-      return ("cmd /c \"" + scriptPath + "\"\n");
-    } else {
-      return ("/bin/sh \"" + scriptPath + "\"\n");
-    }
-  }
-
-  public String getTemplate(String templateName) {
-    try {
-      byte[] bytes = Files.readAllBytes(getTemplatePath(templateName));
-      if (bytes != null) {
-        return new String(bytes);
-      }
-    } catch (IOException e) {
-      service.error(e);
-    }
-    return null;
-  }
-
-  // private void startStreamGobbler(InputStream inputStream, String streamName)
-  // {
-  // executorService.submit(() -> {
-  // new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(line
-  // -> {
-  // System.out.println(line); // Print the line
-  // synchronized (outputCapture) {
-  // outputCapture.append(line).append("\n"); // Capture the line
-  // }
-  // });
-  // });
-  // }
-
-  public Path getTemplatePath(String templateName) {
-    Path scriptPath = Paths.get(service.getResourceDir() + File.separator + "templates" + File.separator, templateName + (isWindows() ? ".bat" : ".sh"));
-    return scriptPath;
-  }
-
-  public String getVersion() {
-    return "0.0.0";
-  }
-
-  public boolean isWindows() {
-    return System.getProperty("os.name").toLowerCase().contains("win");
-  }
-
-  public void processCommand(String input) {
-    processCommand(input, false);
-  }
-
-  public void processCommand(String input, boolean addBoundary) {
-    synchronized (lock) {
-      try {
-        if (input == null) {
-          input = "";
-        }
-        if (process == null) {
-          service.error("cannot process a command when the terminal isn't started");
-          return;
-        }
-        String cmd = null;
-        if (addBoundary) {
-          // windows/mac echo vs linux
-          cmd = String.format("%s\necho %s\n", input, BOUNDARY_MARKER);
-        } else {
-          cmd = String.format("%s\n", input);
-        }
-        lastCmd = cmd;
-        TerminalCmd terminalCmd = new TerminalCmd();
-        terminalCmd.src = service.getName();
-        terminalCmd.terminal = name;
-        terminalCmd.cmd = cmd;
-        service.invoke("publishCmd", terminalCmd);
-        OutputStream outputStream = process.getOutputStream();
-        outputStream.write(cmd.getBytes());
-        outputStream.flush();
-      } catch (Exception e) {
-        service.error(e);
-      }
-    }
-  }
-
-  // FIXME - should be synchronized with
-  public String processBlockingCommand(String input) {
-    synchronized (lock) {
-      blockingOutputQueue.clear();
-      processCommand(input, true);
-      String ret = null;
-      try {
-        while (isRunning && ret == null) {
-          ret = blockingOutputQueue.poll(100, TimeUnit.MILLISECONDS);
-        }
-      } catch (InterruptedException e) {
-        service.error(e);
-      }
-      return ret;
-    }
-  }
-
-  // New method to process a list of commands
-  public void processCommands(List<String> commands) throws IOException {
-    for (String command : commands) {
-      processCommand(command + "\n");
-    }
-  }
-
-  private void shutdownExecutor() {
-    executorService.shutdownNow();
-  }
-
-  public void start() {
-    start(workspace);
-  }
-
-  /**
-   * Start an interactive shell in a workspace directory
-   * 
-   * @param workspace
-   */
-  public void start(String workspace) {
-    if (!isRunning) {
-      synchronized (lock) {
-        try {
-          shellCommand = determineShellCommand();
-          ProcessBuilder processBuilder = new ProcessBuilder(shellCommand.split(" "));
-          processBuilder.redirectErrorStream(true); // Merge stdout and stderr
-
-          if (workspace != null && !workspace.isEmpty()) {
-            this.workspace = workspace;
-            processBuilder.directory(new File(workspace)); // Set the CWD for
-                                                           // the
-                                                           // process
-          }
-
-          process = processBuilder.start();
-          pid = process.pid();
-          isRunning = true;
-
-          startStreamGobbler(process.getInputStream(), "OUTPUT");
-          // FIXME option to attach to stdIn
-          // should
-          // startUserInputForwarder();
-        } catch (Exception e) {
-          isRunning = false;
-          service.error(e);
-        }
-        service.broadcastState();
-      }
-    } else {
-      log.info("{} already started", name);
-    }
-  }
-
-  private void startStreamGobbler(InputStream inputStream, String streamName) {
-    executorService.submit(() -> {
-      try {
-        byte[] buffer = new byte[8192]; // Adjust size as needed
-        int length;
-        StringBuilder dynamicBuffer = new StringBuilder();
-        while ((length = inputStream.read(buffer)) != -1) {
-          String text = new String(buffer, 0, length);
-          // asynchronous publishing of all stdout
-          service.invoke("publishLog", name, text);
-          service.invoke("publishStdOut", text);
-          output.append(text);
-          dynamicBuffer.append(text);
-          System.out.print(text);
-          if (dynamicBuffer.toString().contains(BOUNDARY_MARKER)) {
-            // Boundary marker found, handle command completion here
-            System.out.println("Command execution completed.");
-            // Remove the boundary marker from the output buffer
-            int index = dynamicBuffer.indexOf(BOUNDARY_MARKER);
-            dynamicBuffer.delete(index, index + BOUNDARY_MARKER.length());
-            blockingOutputQueue.add(dynamicBuffer.toString());
-            dynamicBuffer = new StringBuilder();
-          }
-        }
-      } catch (IOException e) {
-        service.error(e);
-      }
-    });
-  }
-
-  private void startUserInputForwarder() {
-    executorService.submit(() -> {
-      try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
-        String inputLine;
-        while ((inputLine = reader.readLine()) != null) {
-          processCommand(inputLine);
-        }
-      } catch (IOException e) {
-        e.printStackTrace();
-      }
-    });
-  }
-
-  public void terminate() throws IOException {
-    synchronized (lock) {
-      // Optionally send a quit command to the shell if it supports graceful
-      // exit.
-      // Example for Unix/Linux/Mac: sendInput("exit\n");
-      // For Windows, it might be different or not necessary.
-      if (process != null) {
-        process.descendants().forEach(processHandle -> {
-          log.info("Terminating PID: " + processHandle.pid());
-          processHandle.destroyForcibly(); // Attempts to terminate the process
-        });
-        // destroying parent
-        process.destroyForcibly();
-        process = null;
-        shutdownExecutor(); // Shutdown the executor service
-      }
-      isRunning = false;
-    }
-    service.broadcastState();
-  }
-
-  public int waitForCompletion() throws InterruptedException {
-    process.waitFor();
-    shutdownExecutor();
-    return process.exitValue();
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index c971d94147..5c6aeb1139 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -56,12 +56,10 @@ public class InMoov2 extends Service<InMoov2Config>
     IKJointAngleListener {
 
   public class Heart implements Runnable {
-    private final ReentrantLock lock = new ReentrantLock();
     private Thread thread;
 
     @Override
     public void run() {
-      if (lock.tryLock()) {
         try {
           while (!Thread.currentThread().isInterrupted()) {
             invoke("publishHeartbeat");
@@ -70,14 +68,12 @@ public void run() {
         } catch (InterruptedException ignored) {
           Thread.currentThread().interrupt();
         } finally {
-          lock.unlock();
           log.info("heart stopping");
           thread = null;
         }
-      }
     }
 
-    public void start() {
+    synchronized public void start() {
       if (thread == null) {
         log.info("starting heart");
         thread = new Thread(this, String.format("%s-heart", getName()));
@@ -88,7 +84,7 @@ public void start() {
       }
     }
 
-    public void stop() {
+    synchronized public void stop() {
       if (thread != null) {
         thread.interrupt();
         config.heartbeat = false;
@@ -274,12 +270,12 @@ public InMoov2Config apply(InMoov2Config c) {
 
       execScript();
 
-      loadAppsScripts();
+//      loadAppsScripts();
 
-      loadInitScripts();
+//      loadInitScripts();
 
       if (c.loadGestures) {
-        loadGestures();
+//        loadGestures();
       }
 
       if (c.heartbeat) {
@@ -1551,7 +1547,7 @@ public String publishFlash(String flashName) {
    * onHeartbeat at a regular interval
    */
   public Heartbeat publishHeartbeat() {
-    log.debug("publishHeartbeat");
+    log.info("publishHeartbeat");
     heartbeatCount++;
     Heartbeat heartbeat = new Heartbeat(this);
     try {
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
index e676c52878..3b74a3bf20 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Arm.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Arm.java
@@ -10,9 +10,7 @@
 import org.myrobotlab.io.FileIO;
 import org.myrobotlab.kinematics.DHLink;
 import org.myrobotlab.kinematics.DHRobotArm;
-import org.myrobotlab.logging.Level;
 import org.myrobotlab.logging.LoggerFactory;
-import org.myrobotlab.logging.LoggingFactory;
 import org.myrobotlab.math.MathUtils;
 import org.myrobotlab.service.config.InMoov2ArmConfig;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -90,7 +88,7 @@ public static DHRobotArm getDHRobotArm(String name, String side) {
 
     return arm;
   }
-
+  
   @Deprecated /* use onMove(map) */
   public void onMoveArm(HashMap<String, Double> map) {
     onMove(map);
@@ -100,6 +98,7 @@ public void onMove(Map<String, Double> map) {
     moveTo(map.get("bicep"), map.get("rotate"), map.get("shoulder"), map.get("omoplate"));
   }
 
+
   /**
    * peer services FIXME - framework should always - startPeers() unless
    * configured not to
@@ -124,6 +123,15 @@ public void startService() {
     shoulder = (ServoControl) startPeer("shoulder");
     omoplate = (ServoControl) startPeer("omoplate");
   }
+  
+  @Override
+  public void stopService() {
+    super.stopService();
+    releasePeer("bicep");
+    releasePeer("rotate");
+    releasePeer("shoulder");
+    releasePeer("omoplate");
+  }
 
   @Override
   public void broadcastState() {
@@ -192,8 +200,8 @@ public ServoControl getRotate() {
 
   public String getScript(String service) {
     String side = getName().contains("left") ? "left" : "right";
-    return String.format("%s.moveArm(\"%s\",%.0f,%.0f,%.0f,%.0f)\n", service, side, bicep.getCurrentInputPos(), rotate.getCurrentInputPos(), shoulder.getCurrentInputPos(),
-        omoplate.getCurrentInputPos());
+    return String.format("%s.moveArm(\"%s\",%.0f,%.0f,%.0f,%.0f)\n", service, side, bicep.getCurrentInputPos(), rotate.getCurrentInputPos(),
+        shoulder.getCurrentInputPos(), omoplate.getCurrentInputPos());
   }
 
   public ServoControl getShoulder() {
@@ -283,6 +291,17 @@ public void onJointAngles(Map<String, Double> angleMap) {
     }
   }
 
+  // FIXME - framework should auto-release - unless configured not to
+  @Override
+  public void releaseService() {
+    try {
+      disable();
+      super.releaseService();
+    } catch (Exception e) {
+      error(e);
+    }
+  }
+
   public void rest() {
     if (bicep != null)
       bicep.rest();
@@ -450,26 +469,4 @@ public void waitTargetPos() {
       omoplate.waitTargetPos();
   }
 
-  public static void main(String[] args) {
-    LoggingFactory.init(Level.INFO);
-
-    try {
-
-      Runtime.main(new String[] { "--log-level", "info", "-s", "inmoov2arm", "InMoov2Arm" });
-      // Runtime.main(new String[] {});
-      // Runtime.main(new String[] { "--install" });
-      InMoov2Arm arm = (InMoov2Arm) Runtime.start("inmoov2arm", "InMoov2Arm");
-      arm.releaseService();
-
-      boolean done = true;
-      if (done) {
-        return;
-      }
-      log.info("leaving main");
-
-    } catch (Exception e) {
-      log.error("main threw", e);
-    }
-  }
-
 }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Hand.java b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
index f7fdadebbc..b30c2bd792 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java
@@ -567,6 +567,20 @@ public List<String> refreshControllers() {
     return controllers;
   }
 
+  public void release() {
+    disable();
+  }
+
+  @Override
+  public void releaseService() {
+    try {
+      disable();
+      super.releaseService();
+    } catch (Exception e) {
+      error(e);
+    }
+  }
+
   public void rest() {
     if (thumb != null)
       thumb.rest();
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Head.java b/src/main/java/org/myrobotlab/service/InMoov2Head.java
index 1e98970f78..f3f5edf366 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Head.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java
@@ -360,6 +360,12 @@ public void release() {
     disable();
   }
 
+  @Override
+  public void releaseService() {
+    disable();
+    super.releaseService();
+  }
+
   public void rest() {
     // initial positions
     // setSpeed(1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
diff --git a/src/main/java/org/myrobotlab/service/InMoov2Torso.java b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
index efdb127957..75fa410ca2 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java
@@ -36,11 +36,27 @@ public InMoov2Torso(String n, String id) {
   @Override
   public void startService() {
     super.startService();
+    
     topStom = (ServoControl) getPeer("topStom");
     midStom = (ServoControl) getPeer("midStom");
     lowStom = (ServoControl) getPeer("lowStom");
   }
 
+  @Override
+  public void releaseService() {
+    try {
+      disable();
+
+      topStom = null;
+      midStom = null;
+      lowStom = null;
+
+      super.releaseService();
+    } catch (Exception e) {
+      error(e);
+    }
+  }
+
   public void enable() {
     if (topStom != null)
       topStom.enable();
@@ -77,7 +93,7 @@ public void disable() {
     if (lowStom != null)
       lowStom.disable();
   }
-
+  
   @Deprecated /* use onMove(map) */
   public void onMoveTorso(HashMap<String, Double> map) {
     onMove(map);
@@ -87,6 +103,7 @@ public void onMove(Map<String, Double> map) {
     moveTo(map.get("topStom"), map.get("midStom"), map.get("lowStom"));
   }
 
+
   public long getLastActivityTime() {
     long minLastActivity = Math.max(topStom.getLastActivityTime(), midStom.getLastActivityTime());
     minLastActivity = Math.max(minLastActivity, lowStom.getLastActivityTime());
@@ -94,7 +111,8 @@ public long getLastActivityTime() {
   }
 
   public String getScript(String inMoovServiceName) {
-    return String.format("%s.moveTorso(%.0f,%.0f,%.0f)\n", inMoovServiceName, topStom.getCurrentInputPos(), midStom.getCurrentInputPos(), lowStom.getCurrentInputPos());
+    return String.format("%s.moveTorso(%.0f,%.0f,%.0f)\n", inMoovServiceName, topStom.getCurrentInputPos(), midStom.getCurrentInputPos(),
+        lowStom.getCurrentInputPos());
   }
 
   public void moveTo(Double topStomPos, Double midStomPos, Double lowStomPos) {
diff --git a/src/main/java/org/myrobotlab/service/TerminalManager.java b/src/main/java/org/myrobotlab/service/TerminalManager.java
deleted file mode 100644
index 69af4548b2..0000000000
--- a/src/main/java/org/myrobotlab/service/TerminalManager.java
+++ /dev/null
@@ -1,236 +0,0 @@
-package org.myrobotlab.service;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentSkipListMap;
-
-import org.myrobotlab.framework.Instantiator;
-import org.myrobotlab.framework.Service;
-import org.myrobotlab.logging.Level;
-import org.myrobotlab.logging.LoggerFactory;
-import org.myrobotlab.logging.LoggingFactory;
-import org.myrobotlab.process.Terminal;
-import org.myrobotlab.process.Terminal.TerminalCmd;
-import org.myrobotlab.service.config.TerminalManagerConfig;
-import org.slf4j.Logger;
-
-public class TerminalManager extends Service<TerminalManagerConfig> {
-
-  public class TerminalLogEntry {
-    public String msg = null;
-    public String src = null;
-    // FIXME - STDERR at some point
-    public String stream = "stdout";
-    public String terminal = null;
-    public long ts = System.currentTimeMillis();
-  }
-
-  public static class TerminalStartupConfig {
-    public String type = null; // Python Node Ros
-
-  }
-
-  public final static Logger log = LoggerFactory.getLogger(TerminalManager.class);
-
-  private static final long serialVersionUID = 1L;
-
-  /**
-   * Thread safe map of all the terminals
-   */
-  protected Map<String, Terminal> terminals = new ConcurrentSkipListMap<>();
-
-  public TerminalManager(String n, String id) {
-    super(n, id);
-  }
-
-  public void deleteTerminal(String name) {
-    log.info("deleting terminal {}", name);
-    if (terminals.containsKey(name)) {
-      terminals.remove(name);
-    } else {
-      info("%s terminal does not exist", name);
-    }
-  }
-
-  /**
-   * Process blocking command in default terminal
-   * 
-   * @param cmd
-   * @return
-   */
-  public String processBlockingCommand(String cmd) {
-    return processBlockingCommand("default", cmd);
-  }
-  
-  /**
-   * Publishes the current command from a terminal
-   * @param cmd
-   * @return
-   */
-  public TerminalCmd publishCmd(TerminalCmd cmd) {
-    return cmd;
-  }
-
-  /**
-   * Synchronously process a command in the terminal
-   * 
-   * @param name
-   * @param cmd
-   */
-  public String processBlockingCommand(String name, String cmd) {
-    if (!terminals.containsKey(name)) {
-      error("could not find terminal %s to process command %s", name, cmd);
-      return null;
-    }
-    Terminal terminal = terminals.get(name);
-    return terminal.processBlockingCommand(cmd);
-  }
-
-  /**
-   * Asynchronously process command in default terminal
-   * 
-   * @param cmd
-   */
-  public void processCommand(String cmd) {
-    processCommand("default", cmd);
-  }
-
-  /**
-   * Process a command against a named terminal
-   * 
-   * @param name
-   * @param cmd
-   */
-  public void processCommand(String name, String cmd) {
-    if (!terminals.containsKey(name)) {
-      error("could not find terminal %s to process command %s", name, cmd);
-      return;
-    }
-    Terminal terminal = terminals.get(name);
-    terminal.processCommand(cmd);
-  }
-
-  /**
-   * Structured log publishing
-   * 
-   * @param name
-   * @param msg
-   * @return
-   */
-  public TerminalLogEntry publishLog(String name, String msg) {
-    TerminalLogEntry entry = new TerminalLogEntry();
-    entry.src = getName();
-    entry.terminal = name;
-    entry.msg = msg;
-    entry.stream = "stdout";
-    return entry;
-  }
-
-  /**
-   * All stdout/stderr from all terminals is published here
-   * 
-   * @param msg
-   * @return
-   */
-  public String publishStdOut(String msg) {
-    return msg;
-  }
-
-  /**
-   * Save configuration of the terminal including if its currently running
-   * 
-   * @param name
-   *          terminal name
-   */
-  public void saveTerminal(String name) {
-    log.info("saving terminal {}", name);
-    // TODO - get terminal startup info and
-    // save it to config
-  }
-
-  public void startTerminal() {
-    startTerminal("default");
-  }
-
-  /**
-   * Start a generalized simple terminal
-   * 
-   * @param name
-   *          terminal name
-   */
-  public void startTerminal(String name) {
-    startTerminal(name, null, null);
-  }
-
-  public void startTerminal(String name, String workspace, String type) {
-    log.info("starting terminal {} {}", name, type);
-
-    Terminal terminal = null;
-    String fullType = null;
-
-    if (type == null) {
-      type = "";
-    }
-
-    if (workspace == null) {
-      workspace = ".";
-    }
-
-    if (!type.contains(".")) {
-      fullType = "org.myrobotlab.process." + type + "Terminal";
-    } else {
-      fullType = type;
-    }
-
-    if (terminals.containsKey(name)) {
-      terminal = terminals.get(name);
-    } else {
-      terminal = (Terminal) Instantiator.getNewInstance(fullType, this, name);
-      terminals.put(name, terminal);
-    }
-    terminal.start(workspace);
-  }
-
-  /**
-   * Terminates the terminal
-   * 
-   * @param name
-   *          terminal name
-   */
-  public void terminateTerminal(String name) {
-    log.info("terminating terminal {}", name);
-    if (terminals.containsKey(name)) {
-      try {
-        Terminal terminal = terminals.get(name);
-        terminal.terminate();
-      } catch (Exception e) {
-        error(e);
-      }
-    } else {
-      info("%s terminal does not exist", name);
-    }
-  }
-
-  public static void main(String[] args) {
-    try {
-
-      LoggingFactory.init(Level.INFO);
-
-      TerminalManager manager = (TerminalManager) Runtime.start("manager", "TerminalManager");
-      Runtime.start("webgui", "WebGui");
-      manager.startTerminal();
-
-//      for (int i = 0; i < 100; ++i) {
-//        String ls = manager.processBlockingCommand("ls");
-//        manager.processCommand("ls");
-//      }
-      
-//      List<String> commands = Arrays.asList("echo Hello", "ls");
-//      manager.processCommands(commands);
-
-
-    } catch (Exception e) {
-      log.error("main threw", e);
-    }
-  }
-
-}
diff --git a/src/main/java/org/myrobotlab/service/WebGui.java b/src/main/java/org/myrobotlab/service/WebGui.java
index 5c1dde8ec0..4188def30d 100644
--- a/src/main/java/org/myrobotlab/service/WebGui.java
+++ b/src/main/java/org/myrobotlab/service/WebGui.java
@@ -943,7 +943,7 @@ public void sendRemote(Message msg) {
       String json = CodecUtils.toJsonMsg(msg);
 
       if (json.length() > maxMsgSize) {
-        log.warn(String.format("sendRemote default msg size (%d) exceeded 65536 for msg %s", json.length(), msg));
+        log.info(String.format("sendRemote default msg size (%d) exceeded 65536 for msg %s", json.length(), msg));
         /*
          * debugging large msgs try {
          * FileIO.toFile(String.format("too-big-%s-%d.json", msg.method,
diff --git a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
index bcff490640..2e21ee2b67 100644
--- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
@@ -11,6 +11,9 @@ public class AudioFileConfig extends ServiceConfig {
   public double volume = 1.0;
   
   public String currentPlaylist = "default";
+
+  @Deprecated /* use regular "listeners" from ServiceConfig parent */
+  public String[] audioListeners;
   
   /**
    * Named map of lists of files
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index b043165cf6..bf611a1fd2 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -236,7 +236,7 @@ public Plan getDefault(Plan plan, String name) {
     }
 
     mouthControl.mouth = i01Name + ".mouth";
-
+    
     UltrasonicSensorConfig ultrasonicLeft = (UltrasonicSensorConfig) plan.get(getPeerName("ultrasonicLeft"));
     ultrasonicLeft.triggerPin = 64;
     ultrasonicLeft.echoPin = 63;
@@ -244,7 +244,8 @@ public Plan getDefault(Plan plan, String name) {
     UltrasonicSensorConfig ultrasonicRight = (UltrasonicSensorConfig) plan.get(getPeerName("ultrasonicRight"));
     ultrasonicRight.triggerPin = 64;
     ultrasonicRight.echoPin = 63;
-
+    
+    
     ProgramABConfig chatBot = (ProgramABConfig) plan.get(getPeerName("chatBot"));
 
     chatBot.bots.add("resource/ProgramAB/Alice");
@@ -265,7 +266,7 @@ public Plan getDefault(Plan plan, String name) {
     chatBot.bots.add("resource/ProgramAB/tr-TR");
 
     Runtime runtime = Runtime.getInstance();
-    String[] bots = new String[] { "cn-ZH", "en-US", "fi-FI", "hi-IN", "nl-NL", "pl-PL", "ru-RU", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-PT", "tr-TR" };
+    String[] bots = new String[] { "cn-ZH", "en-US", "fi-FI", "hi-IN", "nl-NL", "pl-PL","ru-RU", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-PT", "tr-TR" };
     String tag = runtime.getLocaleTag();
     if (tag != null) {
       String[] tagparts = tag.split("-");
@@ -533,7 +534,7 @@ public Plan getDefault(Plan plan, String name) {
     // listeners.add(new Listener("publishProcessMessage",
     // getPeerName("python"), "onPythonMessage"));
     listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
-
+    
     listeners.add(new Listener("publishPython", getPeerName("python")));
 
     // InMoov2 --to--> InMoov2
@@ -545,7 +546,6 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishMoveTorso", getPeerName("torso"), "onMove"));
 
     // service --to--> InMoov2
-
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
 
@@ -562,7 +562,7 @@ public Plan getDefault(Plan plan, String name) {
 
     // Needs upcoming pr
     fsm.listeners.add(new Listener("publishStateChange", name, "publishStateChange"));
-
+    
     // peer --to--> peer
     mouth.listeners.add(new Listener("publishStartSpeaking", name));
     mouth.listeners.add(new Listener("publishStartSpeaking", getPeerName("ear")));
diff --git a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
index c1d82dbe29..d91131ae66 100644
--- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java
@@ -9,6 +9,7 @@ public class ProgramABConfig extends ServiceConfig {
 
   @Deprecated /* unused text filters */
   public String[] textFilters;
+
   /**
    * explicit bot directories
    */
@@ -21,15 +22,16 @@ public class ProgramABConfig extends ServiceConfig {
   public String currentBotName = "Alice";
 
   /**
-   * User name currently interacting with the bot. Setting it here will default
-   * it.
+   * User name currently interacting with the bot. Setting it here will
+   * default it.
    */
   public String currentUserName = "human";
 
   /**
    * sleep current state of the sleep if globalSession is used true : ProgramAB
    * is sleeping and wont respond false : ProgramAB is not sleeping and any
-   * response requested will be processed current sleep/wake value
+   * response requested will be processed
+   * current sleep/wake value
    */
   public boolean sleep = false;
 
diff --git a/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java b/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
index 1eca25bdd0..e79ec273cc 100644
--- a/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java
@@ -10,8 +10,7 @@ public class RemoteSpeechConfig extends SpeechSynthesisConfig {
   public String verb = "GET";
 
   /**
-   * Speech url {text} will be url encoded text that will be transformed to audio
-   * speech
+   * Speech url {text} will be url encoded text that will be transformed to audio speech
    */
   public String url = "http://localhost:5002/api/tts?text={text}";
 
@@ -19,7 +18,7 @@ public class RemoteSpeechConfig extends SpeechSynthesisConfig {
    * Template for POST body, usually JSON, not implemented yet
    */
   public String template = null;
-
+  
   /**
    * Default speech type
    */
diff --git a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
index d7572cd118..99c31bd301 100644
--- a/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/RuntimeConfig.java
@@ -24,7 +24,7 @@ public class RuntimeConfig extends ServiceConfig {
   /**
    * Log level debug, info, warn, error
    */
-  public String logLevel = "warn";
+  public String logLevel = "info";
   
   /**
    * Locale setting for the instance, initial default will be set by the default jvm/os
diff --git a/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java b/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
deleted file mode 100644
index c4e39c6dcd..0000000000
--- a/src/main/java/org/myrobotlab/service/config/TerminalManagerConfig.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.myrobotlab.service.config;
-
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.myrobotlab.service.TerminalManager.TerminalStartupConfig;
-
-public class TerminalManagerConfig extends ServiceConfig {
-
-  Map<String, TerminalStartupConfig> terminals = new TreeMap<>();
-
-}
diff --git a/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java b/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java
deleted file mode 100644
index 28f448218c..0000000000
--- a/src/main/java/org/myrobotlab/service/meta/TerminalManagerMeta.java
+++ /dev/null
@@ -1,23 +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 TerminalManagerMeta extends MetaData {
-  private static final long serialVersionUID = 1L;
-  public final static Logger log = LoggerFactory.getLogger(TerminalManagerMeta.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 TerminalManagerMeta() {
-
-    addDescription("Service that can manage subprocesses");
-    addCategory("programming", "service");
-    setAvailable(true);
-
-  }
-
-}

From 3716a8e9d1fa4a7aa24329c8f0ce7e0e90df7803 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Thu, 25 Apr 2024 21:37:49 -0700
Subject: [PATCH 117/131] starts setup

---
 src/main/java/org/myrobotlab/io/FileIO.java   |    32 +
 .../java/org/myrobotlab/service/InMoov2.java  |   194 +-
 .../org/myrobotlab/service/RemoteSpeech.java  |    15 +-
 .../java/org/myrobotlab/service/Runtime.java  | 10534 ++++++++--------
 .../service/config/InMoov2Config.java         |     2 +
 .../resource/WebGui/app/views/tabsViewCtrl.js |   142 +-
 6 files changed, 5536 insertions(+), 5383 deletions(-)

diff --git a/src/main/java/org/myrobotlab/io/FileIO.java b/src/main/java/org/myrobotlab/io/FileIO.java
index 6e061e8c33..e027aeacd4 100644
--- a/src/main/java/org/myrobotlab/io/FileIO.java
+++ b/src/main/java/org/myrobotlab/io/FileIO.java
@@ -31,6 +31,7 @@
 package org.myrobotlab.io;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -38,6 +39,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.net.URI;
 import java.net.URL;
 import java.nio.charset.Charset;
@@ -1424,5 +1426,35 @@ public static String normalize(String dirPath) {
       return dirPath.replace("\\", "/");
     }
   }
+  
+  public static boolean isExecutableAvailable(String command) {
+    try {
+        // Attempt to execute the command
+        Process process = java.lang.Runtime.getRuntime().exec(command);
+
+        // Check the exit value of the process
+        // If the process has terminated correctly, the command is available
+        if (process.waitFor() == 0) {
+            return true;
+        }
+
+        // Read any errors from the attempted command
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                System.out.println(line);
+            }
+        }
+
+        return false;
+    } catch (IOException e) {
+        log.info("IOException: " + e.getMessage());
+        return false;
+    } catch (InterruptedException e) {
+        log.info("InterruptedException: " + e.getMessage());
+        return false;
+    }
+}
+  
 
 }
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 5c6aeb1139..269d9dc8db 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -14,7 +14,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.commons.io.FilenameUtils;
 import org.myrobotlab.framework.Message;
@@ -52,25 +51,24 @@
 import org.slf4j.Logger;
 
 public class InMoov2 extends Service<InMoov2Config>
-    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider,
-    IKJointAngleListener {
+    implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener {
 
   public class Heart implements Runnable {
     private Thread thread;
 
     @Override
     public void run() {
-        try {
-          while (!Thread.currentThread().isInterrupted()) {
-            invoke("publishHeartbeat");
-            Thread.sleep(config.heartbeatInterval);
-          }
-        } catch (InterruptedException ignored) {
-          Thread.currentThread().interrupt();
-        } finally {
-          log.info("heart stopping");
-          thread = null;
+      try {
+        while (!Thread.currentThread().isInterrupted()) {
+          invoke("publishHeartbeat");
+          Thread.sleep(config.heartbeatInterval);
         }
+      } catch (InterruptedException ignored) {
+        Thread.currentThread().interrupt();
+      } finally {
+        log.info("heart stopping");
+        thread = null;
+      }
     }
 
     synchronized public void start() {
@@ -179,22 +177,12 @@ public static void main(String[] args) {
    */
   protected int bootCount = 0;
 
-  protected transient ProgramAB chatBot;
-
   protected List<String> configList;
 
   protected transient SpeechRecognizer ear;
 
   protected List<LogEntry> errors = new ArrayList<>();
 
-  /**
-   * The finite state machine is core to managing state of InMoov2. There is
-   * very little benefit gained in having the interactions pub/sub. Therefore,
-   * there will be a direct reference to the fsm. If different state graph is
-   * needed, then the fsm can provide that service.
-   */
-  private transient FiniteStateMachine fsm;
-
   // waiting controable threaded gestures we warn user
   protected boolean gestureAlreadyStarted = false;
 
@@ -243,8 +231,7 @@ public static void main(String[] args) {
 
   public InMoov2(String n, String id) {
     super(n, id);
-    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI",
-        "pt-PT", "tr-TR");
+    locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
   }
 
   // should be removed in favor of general listeners
@@ -257,39 +244,16 @@ public void addTextListener(TextListener service) {
   public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
-
-      locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "pl-PL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
-
+      
       if (c.locale != null) {
         setLocale(c.locale);
       } else {
         setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString()));
       }
-      // one way sync configuration into predicates
-      configToPredicates();
-
-      execScript();
-
-//      loadAppsScripts();
-
-//      loadInitScripts();
-
-      if (c.loadGestures) {
-//        loadGestures();
-      }
-
-      if (c.heartbeat) {
-        startHeartbeat();
-      } else {
-        stopHeartbeat();
-      }
-
-      // one way sync configuration into predicates
-      configToPredicates();
 
     } catch (Exception e) {
       error(e);
-    } 
+    }
     return c;
   }
 
@@ -350,15 +314,42 @@ synchronized public void boot() {
         log.warn("will not boot again");
         return;
       }
+      
+      if (bootCount == 0) {
+        info("%s BOOTING ....", getName());
+      }
 
       bootCount++;
-      log.info("boot count {}", bootCount);
+      log.info("BOOT count {}", bootCount);
 
-      // config has not finished processing yet..
       if (runtime.isProcessingConfig()) {
-        log.warn("runtime still processing config set {}, waiting ....", runtime.getConfigName());
+        info("BOOT runtime still processing config set %s, waiting ....", runtime.getConfigName());
+        return;
+      }
+      
+      if (!isReady()) {
+        info("BOOT %s is not yet ready, waiting ....", getName());
         return;
       }
+      
+      info("BOOT starting mandatory services");
+      
+      try {
+        // This is required the core of InMoov is
+        // a FSM ProgramAB and some form of Python/Jython
+        startPeer("fsm");
+
+        // Chatbot is a required part of InMoov2
+        ProgramAB chatBot = (ProgramAB)startPeer("chatBot");
+        chatBot = (ProgramAB) startPeer("chatBot");
+        chatBot.startSession();
+        chatBot.setPredicate("robot", getName());
+      } catch (IOException e) {
+        error(e);
+      }
+      
+      // InMoov2 is now "ready" for mandatory synchronous processing
+      info("BOOT starting scripts");
 
       // check all required services are completely started - or
       // wait/return until they are
@@ -390,7 +381,9 @@ synchronized public void boot() {
        */
 
       // load the InMoov2.py and publish it for Python/Jython or Py4j to consume
-      execScript();
+      if (config.execScript) {
+        execScript();
+      }
 
       // TODO - MAKE BOOT REPORT !!!! deliver it on a heartbeat
       runtime.invoke("publishConfigList");
@@ -582,6 +575,7 @@ public void closeAllImages() {
    */
   public void configToPredicates() {
     log.info("configToPredicates");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
       Class<?> pojoClass = config.getClass();
       Field[] fields = pojoClass.getDeclaredFields();
@@ -686,10 +680,16 @@ public void enable() {
    * @param pythonCode
    * @return
    */
+  @Deprecated /* should publishProcessing or publishPython - no direct handle */
   public boolean exec(String pythonCode) {
     try {
-      Python p = (Python) Runtime.start("python", "Python");
-      return p.exec(pythonCode, true);
+      Python p = (Python) Runtime.getService("python");
+      if (p != null) {
+        return p.exec(pythonCode, true);
+      } else {
+        warn("python not available");
+        return false;
+      }
     } catch (Exception e) {
       error("unable to execute script %s", pythonCode);
       return false;
@@ -763,9 +763,12 @@ public void finishedGesture(String nameOfGesture) {
    * @param event
    */
   public void fire(String event) {
-    // Should this be sent to chatbot too ?
-    // invoke("publishEvent", event);
-    fsm.fire(event);
+    FiniteStateMachine fsm =(FiniteStateMachine)getPeer("fsm");
+    if (fsm != null) {
+      fsm.fire(event);
+    } else {
+      log.warn("cannot fire event %s fsm not ready", event);
+    }
   }
 
   public void fullSpeed() {
@@ -813,18 +816,11 @@ public InMoov2Head getHead() {
    */
   public Long getLastActivityTime() {
     Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null;
-    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime()
-        : null;
-    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime()
-        : null;
-    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null
-        ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime()
-        : null;
-    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null
-        ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime()
-        : null;
-    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime()
-        : null;
+    Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() : null;
+    Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() : null;
+    Long leftHand = (InMoov2Hand) getPeer("leftHand") != null ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() : null;
+    Long rightHand = (InMoov2Hand) getPeer("rightHand") != null ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() : null;
+    Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() : null;
 
     Long lastActivityTime = null;
 
@@ -865,11 +861,24 @@ public OpenCV getOpenCV() {
   }
 
   public String getPredicate(String key) {
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    if (chatBot != null) {
     return getPredicate(chatBot.getConfig().currentUserName, key);
+    } else {
+      log.info("chatBot not ready");
+      return null;
+    }
   }
 
   public String getPredicate(String user, String key) {
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    if (chatBot != null) {
+
     return chatBot.getPredicate(user, key);
+    } else {
+      log.info("chatBot not ready");
+      return null;
+    }
   }
 
   /**
@@ -879,8 +888,15 @@ public String getPredicate(String user, String key) {
    * @return
    */
   public Response getResponse(String text) {
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    if (chatBot != null) {
+
     Response response = chatBot.getResponse(text);
     return response;
+    } else {
+      log.info("chatBot not ready");
+      return null;      
+    }
   }
 
   public InMoov2Arm getRightArm() {
@@ -1058,7 +1074,8 @@ public boolean accept(File dir, String name) {
 
       if (files != null) {
         for (File file : files) {
-          Python p = (Python) Runtime.start("python", "Python");
+          
+          Python p = (Python) Runtime.getService("python");
           if (p != null) {
             p.execFile(file.getAbsolutePath());
           }
@@ -1200,7 +1217,7 @@ public void onEndSpeaking(String utterance) {
    * including lower level logs that do not propegate as statuses
    * 
    * @param log
-   *            - flushed log from Log service
+   *          - flushed log from Log service
    */
   public void onErrors(List<LogEntry> log) {
     errors.addAll(log);
@@ -1222,7 +1239,6 @@ public void onConfigStarted(String configName) {
     invoke("publishConfigStarted", configName);
   }
 
-
   public void onGestureStatus(Status status) {
     if (!status.equals(Status.success()) && !status.equals(Status.warn("Python process killed !"))) {
       error("I cannot execute %s, please check logs", lastGestureExecuted);
@@ -1426,6 +1442,7 @@ public void onText(String text) {
   }
 
   // TODO FIX/CHECK this, migrate from python land
+  @Deprecated /* these are fsm states and should be implemented in python callbacks */
   public void powerDown() {
 
     rest();
@@ -1444,6 +1461,7 @@ public void powerDown() {
 
   // TODO FIX/CHECK this, migrate from python land
   // FIXME - defaultPowerUp switchable + override
+  @Deprecated /* these are fsm states and should be implemented in python callbacks */
   public void powerUp() {
     enable();
     rest();
@@ -1577,8 +1595,7 @@ public Heartbeat publishHeartbeat() {
       }
 
       // interval event firing
-      if (config.stateRandomInterval != null
-          && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
+      if (config.stateRandomInterval != null && System.currentTimeMillis() > stateLastRandomTime + (config.stateRandomInterval * 1000)) {
         // fsm.fire("random");
         stateLastRandomTime = System.currentTimeMillis();
       }
@@ -2008,6 +2025,7 @@ public boolean setPirPlaySounds(boolean b) {
   }
 
   public Object setPredicate(String key, Object data) {
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
     if (data == null) {
       chatBot.setPredicate(key, null); // "unknown" "null" other sillyness ?
     } else {
@@ -2072,7 +2090,12 @@ public boolean setSpeechType(String speechType) {
   }
 
   public void setTopic(String topic) {
-    chatBot.setTopic(topic);
+    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    if (chatBot != null) {
+      chatBot.setTopic(topic);
+    } else {
+      log.info("chatBot not ready");
+    }
   }
 
   public void setTorsoSpeed(Double topStom, Double midStom, Double lowStom) {
@@ -2160,22 +2183,6 @@ public ServiceInterface startPeer(String peer) {
   public void startService() {
     super.startService();
 
-    // This is required the core of InMoov is
-    // a FSM ProgramAB and some form of Python/Jython
-    fsm = (FiniteStateMachine) startPeer("fsm");
-
-    // Chatbot is a required part of InMoov2
-    chatBot = (ProgramAB) startPeer("chatBot");
-    try {
-      chatBot.startSession();
-      chatBot.setPredicate("robot", getName());
-    } catch (IOException e) {
-      error(e);
-    }
-
-    // A python process is required - should be defined as a peer
-    // of Type Python or Py4j
-
     // just for comparing config with current "default"
     // debugging only
     Runtime runtime = Runtime.getInstance();
@@ -2192,7 +2199,6 @@ public void startService() {
     // "subscriptions" in config too ?
     subscribe("runtime", "shutdown");
     subscribe("runtime", "publishConfigList");
-
     runtime.invoke("publishConfigList");
 
     if (config.heartbeat) {
diff --git a/src/main/java/org/myrobotlab/service/RemoteSpeech.java b/src/main/java/org/myrobotlab/service/RemoteSpeech.java
index 3c41b292ca..343e7e2850 100644
--- a/src/main/java/org/myrobotlab/service/RemoteSpeech.java
+++ b/src/main/java/org/myrobotlab/service/RemoteSpeech.java
@@ -17,11 +17,10 @@
 import org.slf4j.Logger;
 
 /**
- * A generalized "remote" speech synthesis interface service. I can be used for
- * potentially many remote TTS services, however, the first one will be
- * MozillaTTS, which we will assume is working locally with docker. See
- * https://github.com/synesthesiam/docker-mozillatts. Example GET:
- * http://localhost:5002/api/tts?text=Hello%20I%20am%20a%20speech%20synthesis%20system%20version%202
+ * A generalized "remote" speech synthesis interface service.  I can be used for potentially many
+ * remote TTS services, however, the first one will be MozillaTTS, which we will assume is 
+ * working locally with docker. See https://github.com/synesthesiam/docker-mozillatts.
+ * Example GET: http://localhost:5002/api/tts?text=Hello%20I%20am%20a%20speech%20synthesis%20system%20version%202
  * 
  * @author GroG
  *
@@ -36,7 +35,7 @@ public class RemoteSpeech extends AbstractSpeechSynthesis<RemoteSpeechConfig> {
    * HttpClient peer for GETs and POSTs
    */
   public transient HttpClient<HttpClientConfig> http = null;
-
+  
   /**
    * Currently only support MozillaTTS
    */
@@ -45,12 +44,12 @@ public class RemoteSpeech extends AbstractSpeechSynthesis<RemoteSpeechConfig> {
   public RemoteSpeech(String n, String id) {
     super(n, id);
   }
-
+  
   @SuppressWarnings({ "unchecked", "rawtypes" })
   @Override
   public void startService() {
     super.startService();
-    http = (HttpClient) startPeer("http");
+    http = (HttpClient)startPeer("http");
   }
 
   public static void main(String[] args) {
diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 579f22d4cf..4b5f418a39 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -130,5224 +130,5320 @@
  * VAR OF RUNTIME !
  *
  */
-public class Runtime extends Service<RuntimeConfig> implements MessageListener, ServiceLifeCyclePublisher,
-		RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider {
-
-	final static private long serialVersionUID = 1L;
-
-	// FIXME - AVOID STATIC FIELDS !!! use .getInstance() to get the singleton
-
-	/**
-	 * a registry of all services regardless of which environment they came from -
-	 * each must have a unique name
-	 */
-	static volatile private Map<String, ServiceInterface> registry = new TreeMap<>();
-
-	/**
-	 * A plan is a request to runtime to change the system. Typically its to ask to
-	 * start and configure new services. The master plan is an accumulation of all
-	 * these requests.
-	 */
-	@Deprecated /* use the filesystem only no memory plan */
-	protected final Plan masterPlan = new Plan("runtime");
-
-	/**
-	 * thread for non-blocking install of services
-	 */
-	static private transient Thread installerThread = null;
-
-	/**
-	 * services which want to know if another service with an interface they are
-	 * interested in registers or is released
-	 *
-	 * requestor type &gt; interface &gt; set of applicable service names
-	 */
-	protected final Map<String, Set<String>> interfaceToNames = new HashMap<>();
-
-	protected final Map<String, Set<String>> typeToNames = new HashMap<>();
-
-	protected final Map<String, Set<String>> interfaceToType = new HashMap<>();
-
-	protected final Map<String, Set<String>> typeToInterface = new HashMap<>();
-
-	private transient static final Object processLock = new Object();
-
-	/**
-	 * FILTERED_INTERFACES are the set of low level interfaces which we are
-	 * interested in filtering out if we want to maintain a data structure which has
-	 * "interfaces of interest"
-	 */
-	protected final static Set<String> FILTERED_INTERFACES = new HashSet<>(Arrays.asList(
-			"org.myrobotlab.framework.interfaces.Broadcaster", "org.myrobotlab.service.interfaces.QueueReporter",
-			"org.myrobotlab.framework.interfaces.ServiceQueue", "org.myrobotlab.framework.interfaces.MessageSubscriber",
-			"org.myrobotlab.framework.interfaces.Invoker", "java.lang.Runnable",
-			"org.myrobotlab.framework.interfaces.ServiceStatus", "org.atmosphere.nettosphere.Handler",
-			"org.myrobotlab.framework.interfaces.NameProvider", "org.myrobotlab.framework.interfaces.NameTypeProvider",
-			"org.myrobotlab.framework.interfaces.ServiceInterface", "org.myrobotlab.framework.interfaces.TaskManager",
-			"org.myrobotlab.framework.interfaces.LoggingSink", "org.myrobotlab.framework.interfaces.StatusPublisher",
-			"org.myrobotlab.framework.interfaces.TypeProvider", "java.io.Serializable",
-			"org.myrobotlab.framework.interfaces.Attachable", "org.myrobotlab.framework.interfaces.StateSaver",
-			"org.myrobotlab.framework.interfaces.MessageSender", "java.lang.Comparable",
-			"org.myrobotlab.service.interfaces.ServiceLifeCycleListener",
-			"org.myrobotlab.framework.interfaces.StatePublisher"));
-
-	protected final Set<String> serviceTypes = new HashSet<>();
-
-	/**
-	 * The directory name currently being used for config. This is NOT full path
-	 * name. It cannot be null, it cannot have "/" or "\" in the name - it has to be
-	 * a valid file name for the OS. It's defaulted to "default". Changed often
-	 */
-	protected static String configName = "default";
-
-	/**
-	 * The runtime config which Runtime was started with. This is the config which
-	 * will be applied to Runtime when its created on startup.
-	 */
-	// protected static RuntimeConfig startConfig = null;
-
-	/**
-	 * State variable reporting if runtime is currently starting services from
-	 * config. If true you can find which config from runtime.getConfigName()
-	 */
-	boolean processingConfig = false;
-
-	/**
-	 * <pre>
-	 * The set of client connections to this mrl instance Some of the connections
-	 * are outbound to other webguis, others may be inbound if a webgui is
-	 * listening in this instance. These details and many others (such as from
-	 * which connection a client is from) is in the Map &lt;String, Object&gt; information.
-	 * Since different connections have different requirements, and details regarding
-	 * clients the only "fixed" required info to add a client is :
-	 *
-	 * uuid - key unique identifier for the client
-	 * connection - name of the connection currently managing the clients connection
-	 * state - state of the client and/or connection
-	 * (lots more attributes with the Map&lt;String, Object&gt; to provide necessary data for the connection)
-	 * </pre>
-	 */
-	protected final Map<String, Connection> connections = new HashMap<>();
-
-	/**
-	 * corrected route table with (soon to be regex ids) mapped to
-	 * gateway/interfaces
-	 */
-	protected final RouteTable routeTable = new RouteTable();
-
-	static private final String RUNTIME_NAME = "runtime";
-
-	/**
-	 * user's data directory
-	 */
-	static public final String DATA_DIR = "data";
-
-	/**
-	 * default parent path of configPath static !
-	 */
-	public final static String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
-
-	/**
-	 * number of services created by this runtime
-	 */
-	protected Integer creationCount = 0;
-
-	/**
-	 * the local repo.json manifest of this machine, which is a list of all
-	 * libraries ivy installed
-	 */
-	transient private IvyWrapper repo = null; // was transient abstract Repo
-
-	transient private ServiceData serviceData = ServiceData.getLocalInstance();
-
-	/**
-	 * command line options
-	 */
-	static CmdOptions options = new CmdOptions();
-
-	/**
-	 * command line configuration
-	 */
-	static StartYml startYml = new StartYml();
-
-	/**
-	 * the platform (local instance) for this runtime. It must be a non-static as
-	 * multiple runtimes will have different platforms
-	 */
-	protected Platform platform = null;
-
-	private static long uniqueID = new Random(System.currentTimeMillis()).nextLong();
-
-	public final static Logger log = LoggerFactory.getLogger(Runtime.class);
-
-	/**
-	 * Object used to synchronize initializing this singleton.
-	 */
-	transient private static final Object INSTANCE_LOCK = new Object();
-
-	/**
-	 * The singleton of this class.
-	 */
-	transient private static Runtime runtime = null;
-
-	private List<String> jvmArgs;
-
-	/**
-	 * set of known hosts
-	 */
-	private transient Map<String, Host> hosts = null;
-
-	/**
-	 * global startingArgs - whatever came into main each runtime will have its
-	 * individual copy
-	 */
-	// FIXME - remove static !!!
-	static String[] globalArgs;
-
-	static Set<String> networkPeers = null;
-
-	/**
-	 * The name of the folder used to store native library dependencies during
-	 * installation and runtime.
-	 */
-	private static final String LIBRARIES = "libraries";
-
-	String stdCliUuid = null;
-
-	InProcessCli cli = null;
-
-	/**
-	 * available Locales
-	 */
-	protected Map<String, Locale> locales;
-
-	protected List<String> configList;
-
-	/**
-	 * Wraps {@link java.lang.Runtime#availableProcessors()}.
-	 *
-	 * @return the number of processors available to the Java virtual machine.
-	 * @see java.lang.Runtime#availableProcessors()
-	 *
-	 */
-	public static final int availableProcessors() {
-		return java.lang.Runtime.getRuntime().availableProcessors();
-	}
-
-	/**
-	 * Function to test if internet connectivity is available. If it is, will return
-	 * the public gateway address of this computer by sending a request to an
-	 * external server. If there is no internet, returns null.
-	 * 
-	 * @return The public IP address or null if no internet available
-	 */
-	static public String getPublicGateway() {
-		try {
-
-			URL url = new URL("http://checkip.amazonaws.com/");
-			HttpURLConnection con = (HttpURLConnection) url.openConnection();
-			con.setRequestMethod("GET");
-
-			int status = con.getResponseCode();
-			log.info("status " + status);
-
-			String gateway = FileIO.toString(con.getInputStream());
-			return gateway;
-
-		} catch (Exception e) {
-			log.warn("internet not available");
-		}
-		return null;
-	}
-
-	/**
-	 * Create which only has name (no type). This is only possible, if there is an
-	 * appropriately named service config in the Plan (in memory) or (more commonly)
-	 * on the filesystem. Since ServiceConfig comes with type information, a name is
-	 * all that is needed to start the service.
-	 * 
-	 * @param name
-	 * @return
-	 */
-	static public ServiceInterface create(String name) {
-		return create(name, null);
-	}
-
-	/**
-	 * Create create(name, type) goes through the full service lifecycle of:
-	 *
-	 * <pre>
-	 * clear - clearing the plan for construction of service(s) needed 
-	 * load  - loading the plan for desired services 
-	 * check - checking all planned service have met appropriate licensing and dependency checks create -
-	 * </pre>
-	 *
-	 * @param name - Required, cannot be null
-	 * @param type - Can be null if a service file exists for named service
-	 * @return the service
-	 */
-	static public ServiceInterface create(String name, String type) {
-
-		synchronized (processLock) {
-
-			try {
-				ServiceInterface si = Runtime.getService(name);
-				if (si != null) {
-					return si;
-				}
-
-				Plan plan = Runtime.load(name, type);
-				Runtime.check(name, type);
-				// at this point - the plan should be loaded, now its time to create the
-				// children peers
-				// and parent service
-				createServicesFromPlan(plan, null, name);
-				si = Runtime.getService(name);
-				if (si == null) {
-					Runtime.getInstance().error("coult not create %s of type %s", name, type);
-				}
-				return si;
-			} catch (Exception e) {
-				runtime.error(e);
-			}
-			return null;
-		}
-	}
-
-	/**
-	 * Creates all services necessary for this service - "all peers" and the parent
-	 * service too. At this point all type information and configuration should be
-	 * defined in the plan.
-	 * 
-	 * FIXME - should Plan be passed in as param ?
-	 *
-	 * @param name
-	 * @return
-	 */
-	private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan,
-			Map<String, ServiceInterface> createdServices, String name) {
-
-		synchronized (processLock) {
-
-			if (createdServices == null) {
-				createdServices = new LinkedHashMap<>();
-			}
-
-			// Plan's config
-			RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
-			// current Runtime config
-			RuntimeConfig currentConfig = Runtime.getInstance().config;
-
-			for (String service : plansRtConfig.getRegistry()) {
-				ServiceConfig sc = plan.get(service);
-				if (sc == null) {
-					runtime.error("could not get %s from plan", service);
-					continue;
-				}
-				ServiceInterface si = createService(service, sc.type, null);
-				// process the base listeners/subscription of ServiceConfig
-				si.addConfigListeners(sc);
-				if (si instanceof ConfigurableService) {
-					try {
-						((ConfigurableService) si).apply(sc);
-					} catch (Exception e) {
-						Runtime.getInstance().error(
-								"could not apply config of type %s to service %s, using default config", sc.type,
-								si.getName(), sc.type);
-					}
-				}
-				createdServices.put(service, si);
-				currentConfig.add(service);
-			}
-
-			return createdServices;
-		}
-	}
-
-	public String getServiceExample(String serviceType) {
-		String url = "https://raw.githubusercontent.com/MyRobotLab/myrobotlab/develop/src/main/resources/resource/"
-				+ serviceType + "/" + serviceType + ".py";
-		byte[] bytes = Http.get(url);
-		if (bytes != null) {
-			return new String(bytes);
-		}
-		return "";
-	}
-
-	public static String getPeerName(String peerKey, ServiceConfig config, Map<String, ServiceReservation> peers,
-			String parentName) {
-
-		if (peerKey == null || !peers.containsKey(peerKey)) {
-			return null;
-		}
-
-		if (config != null) {
-
-			// dynamically get config peer name
-			// e.g. tilt should be a String value in config.tilt
-			Field[] fs = config.getClass().getDeclaredFields();
-			for (Field f : fs) {
-				if (peerKey.equals(f.getName())) {
-					if (f.canAccess(config)) {
-						Object o;
-						try {
-							o = f.get(config);
-
-							if (o == null) {
-								// config "has" the field, just set to null at the moment
-								// peer actual name then will be default notation
-								if (parentName != null) {
-									return String.format("%s.%s", parentName, peerKey);
-								}
-								log.warn("config has field named {} but it's null", peerKey);
-								return null;
-							}
-
-							if (o instanceof String) {
-								return (String) o;
-							} else {
-								log.error("config has field named {} but it is not a string", peerKey);
-								break;
-							}
-						} catch (Exception e) {
-							log.error("getting access to field threw", e);
-						}
-
-					} else {
-						log.error("config with field name {} but cannot access it", peerKey);
-					}
-				}
-			}
-		}
-		// last ditch attempt at getting the name - will default it if parentName is
-		// supplied
-		if (parentName != null) {
-			return String.format("%s.%s", parentName, peerKey);
-		}
-		return null;
-	}
-
-	public static void check(String name, String type) {
-		log.info("check - implement - dependencies and licensing");
-		// iterate through plan - check dependencies and licensing
-	}
-
-	/**
-	 * Use {@link #start(String, String)} instead.
-	 *
-	 * @param name Name of service
-	 * @param type Type of service
-	 * @return Created service
-	 */
-	@Deprecated /* use start */
-	static public ServiceInterface createAndStart(String name, String type) {
-		return start(name, type);
-	}
-
-	/**
-	 * creates and starts services from a cmd line object
-	 *
-	 * @param services - services to be created
-	 */
-	public final static void createAndStartServices(List<String> services) {
-
-		if (services == null) {
-			log.error("createAndStartServices(null)");
-			return;
-		}
-
-		log.info("services {}", Arrays.toString(services.toArray()));
-
-		if (services.size() % 2 == 0) {
-
-			for (int i = 0; i < services.size(); i += 2) {
-				String name = services.get(i);
-				String type = services.get(i + 1);
-
-				log.info("attempting to invoke : {} of type {}", name, type);
-
-				ServiceInterface s = Runtime.create(name, type);
-
-				if (s != null) {
-					try {
-						s.startService();
-					} catch (Exception e) {
-						runtime.error(e.getMessage());
-						Logging.logError(e);
-					}
-				} else {
-					runtime.error(String.format("could not create service %s %s", name, type));
-				}
-
-			}
-			return;
-		}
-		Runtime.mainHelp();
-		shutdown();
-	}
-
-	/**
-	 * Setting the runtime virtual will set the platform virtual too. All subsequent
-	 * services will be virtual
-	 */
-	@Override
-	public boolean setVirtual(boolean b) {
-		boolean changed = config.virtual != b;
-		config.virtual = b;
-		isVirtual = b;
-		setAllVirtual(b);
-		if (changed) {
-			broadcastState();
-		}
-		return b;
-	}
-
-	/**
-	 * Sets all services' virtual state to {@code b}. This allows a single call to
-	 * enable or disable virtualization across all services.
-	 *
-	 * @param b Whether all services should be virtual or not
-	 * @return b
-	 */
-	static public boolean setAllVirtual(boolean b) {
-		for (ServiceInterface si : getServices()) {
-			if (!si.isRuntime()) {
-				si.setVirtual(b);
-			}
-		}
-		Runtime.getInstance().config.virtual = b;
-		Runtime.getInstance().broadcastState();
-		return b;
-	}
-
-	/**
-	 * Sets the enable value in start.yml. start.yml is a file which can control the
-	 * automatic loading of config. In general when its on, and a config is selected
-	 * and saved, the next time Runtime starts it will attempt to load the last
-	 * saved config and get the user back to their last state.
-	 * 
-	 * @param autoStart
-	 * @throws IOException - thrown if cannot write file to filesystem
-	 */
-	public void setAutoStart(boolean autoStart) throws IOException {
-		log.debug("setAutoStart {}", autoStart);
-		startYml.enable = autoStart;
-		startYml.config = configName;
-		FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
-		invoke("getStartYml");
-	}
-
-	/**
-	 * Framework owned method - core of creating a new service. This method will
-	 * create a service with the given name and of the given type. If the type does
-	 * not contain any dots, it will be assumed to be in the
-	 * {@code org.myrobotlab.service} package. This method can currently only
-	 * instantiate Java services, but in the future it could be enhanced to call
-	 * native service runtimes.
-	 * <p>
-	 * The name parameter must not contain '/' or '@'. Thus, a full name must be
-	 * split into its first and second part, passing the first in as the name and
-	 * the second as the inId. This method will log an error and return null if name
-	 * contains either of those two characters.
-	 * <p>
-	 * The {@code inId} is used to determine whether the service is a local one or a
-	 * remote proxy. It should equal the Runtime ID of the MyRobotLab instance the
-	 * service was originally instantiated under.
-	 * 
-	 * @param name May not contain '/' or '@', i.e. cannot be a full name
-	 * @param type The type of the new service
-	 * @param inId The ID of the runtime the service is linked to.
-	 * @return An existing service if the requested name and type match, otherwise a
-	 *         newly created service. If the name is null, or it contains '@' or
-	 *         '/', or a service with the same name exists but has a different type,
-	 *         will return null instead.
-	 */
-	static private ServiceInterface createService(String name, String type, String inId) {
-		synchronized (processLock) {
-			log.info("Runtime.createService {}", name);
-
-			if (name == null) {
-				runtime.error("service name cannot be null");
-
-				return null;
-			}
-
-			if (name.contains("@") || name.contains("/")) {
-				runtime.error("service name cannot contain '@' or '/': {}", name);
-
-				return null;
-			}
-
-			String fullName;
-			if (inId == null || inId.equals(""))
-				fullName = getFullName(name);
-			else
-				fullName = String.format("%s@%s", name, inId);
-
-			if (type == null) {
-				ServiceConfig sc;
-				try {
-					sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
-				} catch (IOException e) {
-					runtime.error("could not find type for service %s", name);
-					return null;
-				}
-				if (sc != null) {
-					log.info("found type for {} in plan", name);
-					type = sc.type;
-				} else {
-					runtime.error("createService type not specified and could not get type for {} from plan", name);
-					return null;
-				}
-			}
-
-			if (type == null) {
-				runtime.error("cannot create service {} no type in plan or yml file", name);
-				return null;
-			}
-
-			String fullTypeName = CodecUtils.makeFullTypeName(type);
-
-			ServiceInterface si = Runtime.getService(fullName);
-			if (si != null) {
-				if (!si.getTypeKey().equals(fullTypeName)) {
-					runtime.error("Service with name {} already exists but is of type {} while requested type is ",
-							name, si.getTypeKey(), type);
-					return null;
-				}
-				return si;
-			}
-
-			// DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
-			// only try to resolve type by the plan - if not then error out
-
-			String id = (inId == null) ? Runtime.getInstance().getId() : inId;
-			if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
-				log.error("{} not a type or {} not defined ", fullTypeName, name);
-				return null;
-			}
-
-			// TODO - test new create of existing service
-			ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
-			if (sw != null) {
-				log.info("service {} already exists", name);
-				return sw;
-			}
-
-			try {
-
-				if (log.isDebugEnabled()) {
-					// TODO - determine if there have been new classes added from
-					// ivy --> Boot Classloader --> Ext ClassLoader --> System
-					// ClassLoader
-					// http://blog.jamesdbloom.com/JVMInternals.html
-					log.debug("ABOUT TO LOAD CLASS");
-					log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
-					log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
-					log.debug("system class loader " + ClassLoader.getSystemClassLoader());
-					log.debug("parent should be null"
-							+ ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
-					log.debug("thread context "
-							+ Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
-					log.debug("thread context parent "
-							+ Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
-				}
-
-				// FIXME - error if deps are missing - prompt license
-				// require restart !
-				// FIXME - this should happen after inspecting the "loaded" "plan" not
-				// during the create/start/apply !
-
-				// create an instance
-				Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
-				log.debug("returning {}", fullTypeName);
-				si = (ServiceInterface) newService;
-
-				// si.setId(id);
-				if (Runtime.getInstance().getId().equals(id)) {
-					si.setVirtual(Runtime.getInstance().isVirtual());
-					Runtime.getInstance().creationCount++;
-					si.setOrder(Runtime.getInstance().creationCount);
-				}
-
-				if (runtime != null) {
-
-					runtime.invoke("created", getFullName(name));
-
-					// add all the service life cycle subscriptions
-					// runtime.addListener("registered", name);
-					// runtime.addListener("created", name);
-					// runtime.addListener("started", name);
-					// runtime.addListener("stopped", name);
-					// runtime.addListener("released", name);
-				}
-
-				return (Service) newService;
-			} catch (Exception e) {
-				log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
-			}
-			return null;
-		}
-	}
-
-	static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries() {
-		return getNotifyEntries(null);
-	}
-
-	static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries(String service) {
-		Map<String, Map<String, List<MRLListener>>> ret = new TreeMap<String, Map<String, List<MRLListener>>>();
-		Map<String, ServiceInterface> sorted = null;
-		if (service == null) {
-			sorted = getLocalServices();
-		} else {
-			sorted = new HashMap<String, ServiceInterface>();
-			ServiceInterface si = Runtime.getService(service);
-			if (si != null) {
-				sorted.put(service, si);
-			}
-		}
-		for (Map.Entry<String, ServiceInterface> entry : sorted.entrySet()) {
-			log.info(entry.getKey() + "/" + entry.getValue());
-			List<String> flks = entry.getValue().getNotifyListKeySet();
-			Map<String, List<MRLListener>> subret = new TreeMap<String, List<MRLListener>>();
-			for (String sn : flks) {
-				List<MRLListener> mrllistners = entry.getValue().getNotifyList(sn);
-				subret.put(sn, mrllistners);
-			}
-			ret.put(entry.getKey(), subret);
-		}
-		return ret;
-	}
-
-	/**
-	 * Dumps {@link #registry} to a file called {@code registry.json} in JSON form.
-	 *
-	 * @return The registry in JSON form or null if an error occurred.
-	 */
-	public static String dump() {
-		try {
-			FileOutputStream dump = new FileOutputStream("registry.json");
-			String reg = CodecUtils.toJson(registry);
-			dump.write(reg.getBytes());
-			dump.close();
-			return reg;
-		} catch (Exception e) {
-			log.error("dump threw", e);
-		}
-		return null;
-	}
-
-	/**
-	 * Wraps {@link java.lang.Runtime#gc()}.
-	 *
-	 * Runs the garbage collector.
-	 */
-	public static final void gc() {
-		java.lang.Runtime.getRuntime().gc();
-	}
-
-	/**
-	 * Although "fragile" since it relies on a external source - its useful to find
-	 * the external ip address of NAT'd systems
-	 *
-	 * @return external or routers ip
-	 * @throws Exception e
-	 */
-	public static String getExternalIp() throws Exception {
-		URL whatismyip = new URL("http://checkip.amazonaws.com");
-		BufferedReader in = null;
-		try {
-			in = new BufferedReader(new InputStreamReader(whatismyip.openStream()));
-			String ip = in.readLine();
-			return ip;
-		} finally {
-			if (in != null) {
-				try {
-					in.close();
-				} catch (IOException e) {
-					e.printStackTrace();
-				}
-			}
-		}
-	}
-
-	/**
-	 * Wraps {@link java.lang.Runtime#freeMemory()}.
-	 *
-	 * @return the amount of free memory in the Java Virtual Machine. Calling the gc
-	 *         method may result in increasing the value returned by freeMemory.
-	 *
-	 *
-	 */
-	public static final long getFreeMemory() {
-		return java.lang.Runtime.getRuntime().freeMemory();
-	}
-
-	/**
-	 * Get a handle to the Runtime singleton.
-	 *
-	 * @return the Runtime
-	 */
-	public static Runtime getInstance() {
-		if (runtime == null) {
-			synchronized (INSTANCE_LOCK) {
-				try {
-
-					RuntimeConfig c = null;
-					if (runtime == null) {
-						c = ConfigUtils.loadRuntimeConfig(options);
-						runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", c.id);
-						runtime.startService();
-						// klunky
-						Runtime.register(new Registration(runtime));
-					}
-
-					runtime.locales = Locale.getDefaults();
-
-					runtime.getRepo().addStatusPublisher(runtime);
-					runtime.startService();
-					// extract resources "if a jar"
-					FileIO.extractResources();
-					runtime.startInteractiveMode();
-					if (c != null) {
-						runtime.apply(c);
-					}
-
-					if (options.services != null && options.services.size() != 0) {
-						log.info("command line services were specified");
-						createAndStartServices(options.services);
-					}
-
-					if (options.config != null) {
-						log.info("command line -c config was specified");
-						Runtime.startConfig(options.config);
-					}
-
-					if (startYml.enable && startYml.config != null) {
-						log.info("start.yml is enabled and config is {}", startYml.config);
-						Runtime.startConfig(startYml.config);
-					}
-
-				} catch (Exception e) {
-					log.error("runtime getInstance threw", e);
-				}
-			} // synchronized lock
-		}
-
-		return runtime;
-	}
-
-	/**
-	 * The jvm args which started this process
-	 *
-	 * @return all jvm args in a list
-	 */
-	static public List<String> getJvmArgs() {
-		RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
-		return runtimeMxBean.getInputArguments();
-	}
-
-	/**
-	 * gets all non-loopback, active, non-virtual ip addresses
-	 *
-	 * @return list of local ipv4 IP addresses
-	 */
-	static public List<String> getIpAddresses() {
-		log.debug("getLocalAddresses");
-		ArrayList<String> ret = new ArrayList<String>();
-
-		try {
-			Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
-			while (interfaces.hasMoreElements()) {
-				NetworkInterface current = interfaces.nextElement();
-				// log.info(current);
-				if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
-					log.debug("skipping interface is down, a loopback or virtual");
-					continue;
-				}
-				Enumeration<InetAddress> addresses = current.getInetAddresses();
-				while (addresses.hasMoreElements()) {
-					InetAddress currentAddress = addresses.nextElement();
-
-					if (!(currentAddress instanceof Inet4Address)) {
-						log.debug("not ipv4 skipping");
-						continue;
-					}
-
-					if (currentAddress.isLoopbackAddress()) {
-						log.debug("skipping loopback address");
-						continue;
-					}
-					log.debug(currentAddress.getHostAddress());
-					ret.add(currentAddress.getHostAddress());
-				}
-			}
-		} catch (Exception e) {
-			Logging.logError(e);
-		}
-
-		if (ret.size() == 0) {
-			// if we don't have a "real" ip address - we always have home
-			ret.add("127.0.0.1");
-		}
-		return ret;
-	}
-
-	// What's the purpose of this? It doesn't return anything
-	static public void getNetInfo() {
-		try {
-			List<String> local = getIpAddresses();
-			String gateway = getPublicGateway();
-			getNetworkPeers();
-		} catch (Exception e) {
-			log.error("getNetInfo threw", e);
-		}
-
-	}
-
-	// TODO - add network to search
-	static public Set<String> getNetworkPeers() throws UnknownHostException {
-		networkPeers = new TreeSet<>();
-		// String myip = InetAddress.getLocalHost().getHostAddress();
-		List<String> myips = getIpAddresses(); // TODO - if nothing else -
-		// 127.0.0.1
-		for (String myip : myips) {
-			if (myip.equals("127.0.0.1")) {
-				log.info("This PC is not connected to any network!");
-			} else {
-				String testIp = null;
-				for (int i = myip.length() - 1; i >= 0; --i) {
-					if (myip.charAt(i) == '.') {
-						testIp = myip.substring(0, i + 1);
-						break;
-					}
-				}
-
-				log.info("My Device IP: " + myip + "\n");
-				log.info("Search log:");
-
-				for (int i = 1; i <= 254; ++i) {
-					try {
-
-						InetAddress addr = InetAddress.getByName(testIp + new Integer(i).toString());
-
-						if (addr.isReachable(1000)) {
-							log.info("Available: " + addr.getHostAddress());
-							networkPeers.add(addr.getHostAddress());
-						} else {
-							log.info("Not available: " + addr.getHostAddress());
-						}
-
-						// TODO - check default port 8888 8887
-
-					} catch (IOException ioex) {
-					}
-				}
-
-				log.info("found {} devices", networkPeers.size());
-
-				for (String device : networkPeers) {
-					log.info(device);
-				}
-			}
-		}
-		return networkPeers;
-	}
-
-	static public List<ApiDescription> getApis() {
-		return CodecUtils.getApis();
-	}
-
-	// @TargetApi(9)
-	static public List<String> getLocalHardwareAddresses() {
-		log.info("getLocalHardwareAddresses");
-		ArrayList<String> ret = new ArrayList<String>();
-		try {
-			Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
-			while (interfaces.hasMoreElements()) {
-				NetworkInterface current = interfaces.nextElement();
-				byte[] mac = current.getHardwareAddress();
-
-				if (mac == null || mac.length == 0) {
-					continue;
-				}
-
-				String m = StringUtil.bytesToHex(mac);
-				log.info("mac address : {}", m);
-				ret.add(m);
-				log.info("added mac");
-			}
-		} catch (Exception e) {
-			log.error("getLocalHardwareAddresses threw", e);
-		}
-
-		log.info("done");
-		return ret;
-	}
-
-	/**
-	 * Gets a Map between service names and the service object of all services local
-	 * to this MRL instance.
-	 * 
-	 * @return A Map between service names and service objects
-	 */
-	public static Map<String, ServiceInterface> getLocalServices() {
-		Map<String, ServiceInterface> local = new HashMap<>();
-		for (String serviceName : registry.keySet()) {
-			// FIXME @ should be a requirement of "all" entries for consistency
-			if (!serviceName.contains("@")
-					|| serviceName.endsWith(String.format("@%s", Runtime.getInstance().getId()))) {
-				local.put(serviceName, registry.get(serviceName));
-			}
-		}
-		return local;
-	}
-
-	/**
-	 * FIXME - return
-	 *
-	 * @return filtering/query requests
-	 */
-	public static Map<String, ServiceInterface> getLocalServicesForExport() {
-		return registry;
-	}
-
-	/*
-	 * FIXME - DEPRECATE - THIS IS NOT "instance" specific info - its Class
-	 * definition info - Runtime should return based on ClassName
-	 *
-	 * FIXME - INPUT PARAMETER SHOULD BE TYPE NOT INSTANCE NAME !!!!
-	 */
-	public static Map<String, MethodEntry> getMethodMap(String inName) {
-		String serviceName = getFullName(inName);
-		if (!registry.containsKey(serviceName)) {
-			runtime.error(String.format("%1$s not in registry - can not return method map", serviceName));
-			return null;
-		}
-
-		ServiceInterface sw = registry.get(serviceName);
-		Class<?> c = sw.getClass();
-
-		MethodCache cache = MethodCache.getInstance();
-		return cache.getRemoteMethods(c.getTypeName());
-
-	}
-
-	/**
-	 * getServiceList returns the most important identifiers for a service which are
-	 * it's process id, it's name, and it's type.
-	 * <p>
-	 * This will be part of the getHelloRequest - and the first listing from a
-	 * process of what services are available.
-	 * <p>
-	 * TODO - future work would be to supply a query to the getServiceList(query)
-	 * such that interfaces, types, or processes ids, can selectively be queried out
-	 * of it
-	 *
-	 * @return list of registrations
-	 */
-	public List<Registration> getServiceList() {
-		synchronized (processLock) {
-			return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey()))
-					.collect(Collectors.toList());
-		}
-	}
-
-	// FIXME - scary function - returns private data
-	public static Map<String, ServiceInterface> getRegistry() {
-		return registry;// FIXME should return copy
-	}
-
-	public static ServiceInterface getService(String inName) {
-		return getService(inName, new StaticType<>() {
-		});
-	}
-
-	public static <C extends ServiceConfig, S extends ServiceInterface & ConfigurableService<C>> S getConfigurableService(
-			String inName, StaticType<S> serviceType) {
-		return getService(inName, serviceType);
-	}
-
-	/**
-	 * Gets a running service with the specified name. If the name is null or
-	 * there's no such service with the specified name, returns null instead.
-	 *
-	 * @param inName The name of the service
-	 * @return The service if it exists, or null
-	 */
-	@SuppressWarnings("unchecked")
-	public static <S extends ServiceInterface> S getService(String inName, StaticType<S> serviceType) {
-		if (inName == null) {
-			return null;
-		}
-
-		String name = getFullName(inName);
-
-		if (!registry.containsKey(name)) {
-			return null;
-		} else {
-			return (S) registry.get(name);
-		}
-	}
-
-	/**
-	 * @return all service names in an array form
-	 * 
-	 *
-	 */
-	static public String[] getServiceNames() {
-		Set<String> ret = registry.keySet();
-		String[] services = new String[ret.size()];
-		if (ret.size() == 0) {
-			return services;
-		}
-
-		// if there are more than 0 services we need runtime
-		// to filter to make sure they are "local"
-		// and this requires a runtime service
-		String localId = Runtime.getInstance().getId();
-		int cnt = 0;
-		for (String fullname : ret) {
-			if (fullname.endsWith(String.format("@%s", localId))) {
-				services[cnt] = CodecUtils.getShortName(fullname);
-			} else {
-				services[cnt] = fullname;
-			}
-			++cnt;
-		}
-		return services;
-	}
-
-	// Is it a good idea to modify all regex inputs? For example, if the pattern
-	// already contains ".?" then the replacement will result in "..?"
-	// If POSIX-style globs are desired there are different
-	// pattern matching engines designed for that
-	public static boolean match(String text, String pattern) {
-		return text.matches(pattern.replace("?", ".?").replace("*", ".*?"));
-	}
-
-	public static List<String> getServiceNames(String pattern) {
-		return getServices().stream().map(NameProvider::getName).filter(serviceName -> match(serviceName, pattern))
-				.collect(Collectors.toList());
-	}
-
-	/**
-	 * @param interfaze the interface
-	 * @return a list of service names that implement the interface
-	 * @throws ClassNotFoundException if the class for the requested interface is
-	 *                                not found.
-	 *
-	 */
-	public static List<String> getServiceNamesFromInterface(String interfaze) throws ClassNotFoundException {
-		if (!interfaze.contains(".")) {
-			interfaze = "org.myrobotlab.service.interfaces." + interfaze;
-		}
-
-		return getServiceNamesFromInterface(Class.forName(interfaze));
-	}
-
-	/**
-	 * @param interfaze interface
-	 * @return list of service names
-	 * 
-	 */
-	public static List<String> getServiceNamesFromInterface(Class<?> interfaze) {
-		return getServicesFromInterface(interfaze).stream().map(ServiceInterface::getFullName)
-				.collect(Collectors.toList());
-	}
-
-	/**
-	 * Get all currently-running services
-	 *
-	 * @return A list of all currently-running services
-	 */
-	public static List<ServiceInterface> getServices() {
-		return getServices(null);
-	}
-
-	/**
-	 * Get all services that belong to an MRL instance with the given ID.
-	 * 
-	 * @param id The ID of the MRL instance
-	 * @return A list of the services that belong to the given MRL instance
-	 */
-	public static List<ServiceInterface> getServices(String id) {
-		if (id == null) {
-			return new ArrayList<ServiceInterface>(registry.values());
-		}
-
-		List<ServiceInterface> list = new ArrayList<>();
-		// otherwise we are getting services of an instance
-
-		for (String serviceName : registry.keySet()) {
-			ServiceInterface si = registry.get(serviceName);
-			if (si.getId().equals(id)) {
-				list.add(registry.get(serviceName));
-			}
-		}
-		return list;
-	}
-
-	/**
-	 * @param interfaze interface
-	 * @return results
-	 *
-	 */
-	public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze) {
-		ServiceTypeNameResults results = new ServiceTypeNameResults(interfaze);
-		try {
-
-			if (!interfaze.contains(".")) {
-				interfaze = "org.myrobotlab.service.interfaces." + interfaze;
-			}
-
-			ServiceData sd = ServiceData.getLocalInstance();
-
-			List<MetaData> sts = sd.getServiceTypes();
-
-			for (MetaData st : sts) {
-
-				Set<Class<?>> ancestry = new HashSet<>();
-				Class<?> targetClass = Class.forName(st.getType()); // this.getClass();
-
-				while (targetClass.getCanonicalName().startsWith("org.myrobotlab")
-						&& !targetClass.getCanonicalName().startsWith("org.myrobotlab.framework")) {
-					ancestry.add(targetClass);
-					targetClass = targetClass.getSuperclass();
-				}
-
-				for (Class<?> c : ancestry) {
-					Class<?>[] interfaces = Class.forName(c.getName()).getInterfaces();
-					for (Class<?> inter : interfaces) {
-						if (interfaze.equals(inter.getName())) {
-							results.serviceTypes.add(st.getType());
-							break;
-						}
-					}
-				}
-			}
-
-		} catch (Exception e) {
-			error("could not find interfaces for %s - %s %s", interfaze, e.getClass().getSimpleName(), e.getMessage());
-			log.error("getting class", e);
-		}
-
-		return results;
-	}
-
-	/**
-	 * return a list of services which are currently running and implement a
-	 * specific interface
-	 *
-	 * @param interfaze class
-	 * @return list of service interfaces
-	 *
-	 */
-	// FIXME !!! - use single implementation that gets parents
-	@Deprecated /*
-				 * no longer used or needed - change events are pushed no longer pulled <-- Over
-				 * complicated solution
-				 */
-	public static List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
-		synchronized (processLock) {
-			List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
-
-			for (String service : getServiceNames()) {
-				Class<?> clazz = getService(service).getClass();
-				while (clazz != null) {
-					for (Class<?> inter : clazz.getInterfaces()) {
-						if (inter.getName().equals(interfaze.getName())) {
-							ret.add(getService(service));
-							continue;
-						}
-					}
-					clazz = clazz.getSuperclass();
-				}
-			}
-			return ret;
-		}
-	}
-
-	/**
-	 * Because startYml is required to be a static variable, since it's needed
-	 * "before" a runtime instance exists it will be null in json serialization.
-	 * This method is needed so we can serialize the data appropriately.
-	 * 
-	 * @return
-	 */
-	static public StartYml getStartYml() {
-		return startYml;
-	}
-
-	/**
-	 * Gets the set of all threads currently running.
-	 * 
-	 * @return A set containing thread objects representing all running threads
-	 */
-	static public Set<Thread> getThreads() {
-		return Thread.getAllStackTraces().keySet();
-	}
-
-	/**
-	 * Wraps {@link java.lang.Runtime#totalMemory()}.
-	 *
-	 * @return The amount of memory available to the JVM in bytes.
-	 */
-	public static final long getTotalMemory() {
-
-		return java.lang.Runtime.getRuntime().totalMemory();
-	}
-
-	/**
-	 * FIXME - terrible use a uuid
-	 * 
-	 * unique id's are need for sendBlocking - to uniquely identify the message this
-	 * is a method to support that - it is unique within a process, but not across
-	 * processes
-	 *
-	 * @return a unique id
-	 */
-	public static final synchronized long getUniqueID() {
-		++uniqueID;
-		return System.currentTimeMillis();
-	}
-
-	/**
-	 * Get how long this MRL instance has been running in human-readable String
-	 * form.
-	 *
-	 * @return The uptime of this instance.
-	 */
-	public static String getUptime() {
-		Date now = new Date();
-		Platform platform = Platform.getLocalInstance();
-		String uptime = getDiffTime(now.getTime() - platform.getStartTime().getTime());
-		log.info("up for {}", uptime);
-		return uptime;
-	}
-
-	public static String getPlatformInfo() {
-		Platform platform = Platform.getLocalInstance();
-		StringBuilder sb = new StringBuilder();
-		sb.append(platform.getHostname());
-		sb.append(" ");
-		sb.append(platform.getOS());
-		sb.append(" ");
-		sb.append(platform.getArch());
-		sb.append(".");
-		sb.append(platform.getOsBitness());
-
-		sb.append(" Java ");
-		sb.append(platform.getVmVersion());
-		sb.append(" ");
-		sb.append(platform.getVMName());
-
-		return sb.toString();
-	}
-
-	/**
-	 * Get a human-readable String form of a difference in time in milliseconds.
-	 *
-	 * @param diff The difference of time in milliseconds
-	 * @return The human-readable string form of the difference in time
-	 */
-	public static String getDiffTime(long diff) {
-
-		long diffSeconds = diff / 1000 % 60;
-		long diffMinutes = diff / (60 * 1000) % 60;
-		long diffHours = diff / (60 * 60 * 1000) % 24;
-		long diffDays = diff / (24 * 60 * 60 * 1000);
-
-		StringBuffer sb = new StringBuffer();
-		sb.append(diffDays).append(" days ").append(diffHours).append(" hours ").append(diffMinutes).append(" minutes ")
-				.append(diffSeconds).append(" seconds");
-		return sb.toString();
-
-	}
-
-	/**
-	 * Get version returns the current version of mrl. It must be done this way,
-	 * because the version may be queried on the command line without the desire to
-	 * start a "Runtime"
-	 *
-	 * @return the version of the running platform instance
-	 *
-	 */
-	public static String getVersion() {
-		return Platform.getLocalInstance().getVersion();
-	}
-
-	/**
-	 * Get the latest version number of MRL in String form by querying the public
-	 * build server. If it cannot be contacted, this method returns the String
-	 * {@code "unknown"}.
-	 * 
-	 * @return The latest build version in String form
-	 */
-	public static String getLatestVersion() {
-		String latest = "https://build.myrobotlab.org:8443/job/myrobotlab/job/develop/lastSuccessfulBuild/buildNumber";
-		byte[] b = Http.get(latest);
-		String version = (b == null) ? "unknown" : "1.1." + new String(b);
-		return version;
-	}
-
-	// FIXME - shouldn't this be in platform ???
-
-	/**
-	 * Get the branch that this installation was built from.
-	 *
-	 * @return The branch
-	 * @see Platform#getBranch()
-	 */
-	public static String getBranch() {
-		return Platform.getLocalInstance().getBranch();
-	}
-
-	/**
-	 * Install all services
-	 *
-	 * @throws ParseException Unknown
-	 * @throws IOException    Unknown
-	 */
-	// TODO: Check throws list to see if these are still thrown
-	static public void install() throws ParseException, IOException {
-		install(null, null);
-	}
-
-	/**
-	 * Install specified service.
-	 *
-	 * @param serviceType Service to install
-	 */
-	static public void install(String serviceType) {
-		install(serviceType, null);
-	}
-
-	/**
-	 * Maximum complexity install - allows for blocking and non-blocking install.
-	 * During typically runtime install of services - non blocking is desired,
-	 * otherwise status info from the install is blocked until installation is
-	 * completed. For command line installation "blocking" mode would be desired
-	 *
-	 * FIXME - problematic in that Runtime.create calls this directly, and this
-	 * should be stepped through, because: If we need to install new components, a
-	 * restart is likely needed ... we don't do custom dynamic classloaders .... yet
-	 *
-	 * License - should be appropriately accepted or rejected by user
-	 *
-	 * @param serviceType the service tyype to install
-	 * @param blocking    if this should block until done.
-	 *
-	 */
-	static public void install(String serviceType, Boolean blocking) {
-		synchronized (processLock) {
-			Runtime r = getInstance();
-
-			if (blocking == null) {
-				blocking = false;
-			}
-
-			if (installerThread != null) {
-				log.error("another request to install dependencies, 1st request has not completed");
-				return;
-			}
-
-			installerThread = new Thread() {
-				@Override
-				public void run() {
-					try {
-						if (serviceType == null) {
-							r.getRepo().install();
-						} else {
-							r.getRepo().install(serviceType);
-						}
-					} catch (Exception e) {
-						r.error("dependencies failed - install error", e);
-						throw new RuntimeException(
-								String.format("dependencies failed - install error %s", e.getMessage()));
-					}
-				}
-			};
-
-			if (blocking) {
-				installerThread.run();
-			} else {
-				installerThread.start();
-			}
-
-			installerThread = null;
-		}
-	}
-
-	/**
-	 * Invoke a service method. The parameter must not be null and must have at
-	 * least 2 elements. The first is the service name and the second is the service
-	 * method. The rest of the elements are parameters to the specified method.
-	 *
-	 * @param invoke The array of service name, method, and parameters
-	 */
-	static public void invokeCommands(String[] invoke) {
-
-		if (invoke.length < 2) {
-			log.error("invalid invoke request, minimally 2 parameters are required: --invoke service method ...");
-			return;
-		}
-
-		String name = invoke[0];
-		String method = invoke[1];
-
-		// params
-		Object[] data = new Object[invoke.length - 2];
-		for (int i = 2; i < invoke.length; ++i) {
-			data[i - 2] = invoke[i];
-		}
-
-		log.info("attempting to invoke : {}.{}({})\n", name, method, Arrays.toString(data));
-		getInstance().send(name, method, data);
-	}
-
-	/**
-	 * Checks if a service is local to this MRL instance. The service must exist.
-	 *
-	 * @param serviceName The name of the service to check
-	 * @return Whether the specified service is local or not
-	 */
-	public static boolean isLocal(String serviceName) {
-		ServiceInterface sw = getService(serviceName);
-		return Objects.equals(sw.getId(), Runtime.getInstance().getId());
-	}
-
-	/*
-	 * check if class is a Runtime class
-	 *
-	 * @return true if class == Runtime.class
-	 */
-	public static boolean isRuntime(Service newService) {
-		return newService.getClass().equals(Runtime.class);
-	}
-
-	/**
-	 * Start interactive mode on {@link System#in} and {@link System#out}.
-	 *
-	 * @see #startInteractiveMode(InputStream, OutputStream)
-	 */
-	public void startInteractiveMode() {
-		startInteractiveMode(System.in, System.out);
-	}
-
-	/**
-	 * Starts an interactive CLI on the specified input and output streams. The CLI
-	 * command processor runs in its own thread and takes commands according to the
-	 * CLI API.
-	 * 
-	 * FIXME - have another shell script which starts jar as ws client with cli
-	 * interface Remove this std in/out - it is overly complex and different OSs
-	 * handle it differently Windows Java updates have broken it several times
-	 *
-	 * @param in  The input stream to take commands from
-	 * @param out The output stream to print command output to
-	 * @return The constructed CLI processor
-	 */
-	public InProcessCli startInteractiveMode(InputStream in, OutputStream out) {
-		if (cli != null) {
-			log.info("already in interactive mode");
-			return cli;
-		}
-
-		cli = new InProcessCli(this, "runtime", in, out);
-		Connection c = cli.getConnection();
-		stdCliUuid = (String) c.get("uuid");
-
-		// addRoute(".*", getName(), 100);
-		addConnection(stdCliUuid, cli.getId(), c);
-
-		return cli;
-	}
-
-	/**
-	 * Stops interactive mode if it's running.
-	 */
-	public void stopInteractiveMode() {
-		if (cli != null) {
-			cli.stop();
-			cli = null;
-		}
-		if (stdCliUuid != null) {
-			removeConnection(stdCliUuid);
-			stdCliUuid = null;
-		}
-	}
-
-	/**
-	 * prints help to the console
-	 */
-	static void mainHelp() {
-		new CommandLine(new CmdOptions()).usage(System.out);
-	}
-
-	/**
-	 * Logs a string message and publishes the message.
-	 *
-	 * @param msg The message to log and publish
-	 * @return msg
-	 */
-	public static String message(String msg) {
-		getInstance().invoke("publishMessage", msg);
-		log.info(msg);
-		return msg;
-	}
-
-	/**
-	 * Listener for state publishing, updates registry
-	 * 
-	 * @param updatedService Updated service to put in the registry
-	 */
-	public void onState(ServiceInterface updatedService) {
-		log.info("runtime updating registry info for remote service {}", updatedService.getName());
-		registry.put(String.format("%s@%s", updatedService.getName(), updatedService.getId()), updatedService);
-	}
-
-	public static Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
-		synchronized (processLock) {
-			Registration proxy = new Registration(id, name, typeKey, interfaces);
-			register(proxy);
-			return proxy;
-		}
-	}
-
-	/**
-	 * Registration is the process where a remote system sends detailed info related
-	 * to its services. It will have details on each service type, state, id, and
-	 * other info. The registration is serializable, with state information in a
-	 * serialized for so that stateless processes or other non-Java instances can
-	 * register or be registered.
-	 *
-	 * Registration might setup subscriptions to support a UI.
-	 *
-	 * Additional info which will be added in the future is a method map (a swagger
-	 * concept) and a list of supported interfaces
-	 *
-	 * TODO - have rules on what registrations to accept - dependent on security,
-	 * desire, re-broadcasting configuration etc. TODO - determine rules on
-	 * re-broadcasting based on configuration
-	 *
-	 * @param registration registration
-	 * @return registration
-	 *
-	 */
-	public static Registration register(Registration registration) {
-		synchronized (processLock) {
-			try {
-
-				// TODO - have rules on what registrations to accept - dependent on
-				// security, desire, re-broadcasting configuration etc.
-
-				String fullname = String.format("%s@%s", registration.getName(), registration.getId());
-				if (registry.containsKey(fullname)) {
-					log.info("{} already registered", fullname);
-					return registration;
-				}
-
-				// if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
-				// log.error("Invalid type key being registered: " +
-				// registration.getTypeKey());
-				// return null;
-				// }
-
-				log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(),
-						ConfigUtils.getId(), registration.getTypeKey());
-
-				if (!registration.isLocal(ConfigUtils.getId())) {
-
-					// Check if we're registering a java service
-					if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
-
-						String fullTypeName;
-						if (registration.getTypeKey().contains(".")) {
-							fullTypeName = registration.getTypeKey();
-						} else {
-							fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
-						}
-
-						try {
-							// de-serialize, class exists
-							registration.service = Runtime.createService(registration.getName(), fullTypeName,
-									registration.getId());
-							if (registration.getState() != null) {
-								copyShallowFrom(registration.service,
-										CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
-							}
-						} catch (ClassNotFoundException classNotFoundException) {
-							log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(),
-									registration.getId(), registration.getTypeKey()), classNotFoundException);
-							return null;
-						}
-					} else {
-						// We're registering a foreign process service. We don't need to
-						// check
-						// ForeignProcessUtils.isForeignTypeKey() because the type key is
-						// valid
-						// but is not a java class name
-
-						// Class does not exist, check if registration has empty interfaces
-						// Interfaces should always include ServiceInterface if coming from
-						// remote client
-						if (registration.interfaces == null || registration.interfaces.isEmpty()) {
-							log.error("Unknown service type being registered, registration does not contain any "
-									+ "interfaces for proxy generation: " + registration.getTypeKey());
-							return null;
-						}
-
-						// FIXME - probably some more clear definition about the
-						// requirements
-						// of remote
-						// service registration
-						// In general, there should be very few requirements if any, besides
-						// providing a
-						// name, and the proxy
-						// interface should be responsible for creating a minimal
-						// interpretation
-						// (ServiceInterface) for the remote
-						// service
-
-						// Class<?>[] interfaces = registration.interfaces.stream().map(i ->
-						// {
-						// try {
-						// return Class.forName(i);
-						// } catch (ClassNotFoundException e) {
-						// throw new RuntimeException("Unable to load interface " + i + "
-						// defined in remote registration " + registration, e);
-						// }
-						// }).toArray(Class<?>[]::new);
-
-						// registration.service = (ServiceInterface)
-						// Proxy.newProxyInstance(Runtime.class.getClassLoader(),
-						// interfaces,
-						// new ProxyServiceInvocationHandler(registration.getName(),
-						// registration.getId()));
-						try {
-							registration.service = ProxyFactory.createProxyService(registration);
-							log.info("Created proxy: " + registration.service);
-						} catch (Exception e) {
-							// at the moment preventing throw
-							Runtime.getInstance().error(e);
-						}
-					}
-				}
-
-				registry.put(fullname, registration.service);
-
-				if (runtime != null) {
-
-					String type = registration.getTypeKey();
-
-					// If type does not exist in typeToNames, make it an empty hash set
-					// and
-					// return it
-					Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
-					names.add(fullname);
-
-					// FIXME - most of this could be static as it represents meta data of
-					// class and interfaces
-
-					// FIXME - was false - setting now to true .. because
-					// 1 edge case - "can something fulfill my need of an interface - is
-					// not
-					// currently
-					// switching to true
-					boolean updatedServiceLists = false;
-
-					// maintaining interface type relations
-					// see if this service type is new
-					// PROCESS INDEXES ! - FIXME - will need this in unregister
-					// ALL CLASS/TYPE PROCESSING only needs to happen once per type
-					if (!runtime.serviceTypes.contains(type)) {
-						// CHECK IF "CAN FULFILL"
-						// add the interfaces of the new service type
-						Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(),
-								FILTERED_INTERFACES);
-						for (String interfaze : interfaces) {
-							Set<String> types = runtime.interfaceToType.get(interfaze);
-							if (types == null) {
-								types = new HashSet<>();
-							}
-							types.add(registration.getTypeKey());
-							runtime.interfaceToType.put(interfaze, types);
-						}
-
-						runtime.typeToInterface.put(type, interfaces);
-						runtime.serviceTypes.add(registration.getTypeKey());
-						updatedServiceLists = true;
-					}
-
-					// check to see if any of our interfaces can fulfill requested ones
-					Set<String> myInterfaces = runtime.typeToInterface.get(type);
-					for (String inter : myInterfaces) {
-						if (runtime.interfaceToNames.containsKey(inter)) {
-							runtime.interfaceToNames.get(inter).add(fullname);
-							updatedServiceLists = true;
-						}
-					}
-
-					if (updatedServiceLists) {
-						runtime.invoke("publishInterfaceToNames");
-					}
-
-					// TODO - determine rules on re-broadcasting based on configuration
-					runtime.invoke("registered", registration);
-				}
-
-				// TODO - remove ? already get state from registration
-				if (!registration.isLocal(ConfigUtils.getId())) {
-					runtime.subscribe(registration.getFullName(), "publishState");
-				}
-
-			} catch (Exception e) {
-				log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
-				return null;
-			}
-
-			return registration;
-		}
-	}
-
-	/**
-	 * releases a service - stops the service, its threads, releases its resources,
-	 * and removes registry entries
-	 *
-	 * FIXME - clean up subscriptions from released
-	 *
-	 * @param inName name to release
-	 * @return true/false
-	 *
-	 */
-	public static boolean releaseService(String inName) {
-		ServiceInterface sc = getService(inName);
-		if (sc != null) {
-			sc.releaseService();
-			return true;
-		}
-		return false;
-	}
-
-	/**
-	 * Called after any subclassed releaseService has been called, this cleans up
-	 * the registry and removes peers
-	 * 
-	 * @param inName
-	 * @return
-	 */
-	public static boolean releaseServiceInternal(String inName) {
-		synchronized (processLock) {
-			if (inName == null) {
-				log.debug("release (null)");
-				return false;
-			}
-
-			String name = getFullName(inName);
-
-			String id = CodecUtils.getId(name);
-			if (!id.equals(Runtime.getInstance().getId())) {
-				log.warn("will only release local services - %s is remote", name);
-				return false;
-			}
-
-			log.info("releasing service {}", name);
-
-			if (!registry.containsKey(name)) {
-				log.info("{} not registered", name);
-				return false;
-			}
-
-			// get reference from registry
-			ServiceInterface si = registry.get(name);
-			if (si == null) {
-				log.warn("cannot release {} - not in registry");
-				return false;
-			}
-
-			// FIXME - TODO invoke and or blocking on preRelease - Future
-
-			// send msg to service to self terminate
-			if (si.isLocal()) {
-				si.purgeTasks();
-				si.stopService();
-			} else {
-				if (runtime != null) {
-					runtime.send(name, "releaseService");
-				}
-			}
-
-			// recursive peer release
-			Map<String, Peer> peers = si.getPeers();
-			if (peers != null) {
-				for (Peer peer : peers.values()) {
-					release(peer.name);
-				}
-			}
-
-			// FOR remote this isn't correct - it should wait for
-			// a message from the other runtime to say that its released
-			unregister(name);
-			return true;
-		}
-	}
-
-	/**
-	 * Removes registration for a service. Removes the service from
-	 * {@link #typeToInterface} and {@link #interfaceToNames}.
-	 * 
-	 * @param inName Name of the service to unregister
-	 */
-	public static void unregister(String inName) {
-		synchronized (processLock) {
-			String name = getFullName(inName);
-			log.info("unregister {}", name);
-
-			// get reference from registry
-			ServiceInterface sw = registry.get(name);
-			if (sw == null) {
-				log.debug("{} already unregistered", name);
-				return;
-			}
-
-			// you have to send released before removing from registry
-			if (runtime != null) {
-				runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
-				// !!
-				// it should be FULLNAME !
-				// runtime.broadcast("released", inName);
-				String type = sw.getTypeKey();
-
-				boolean updatedServiceLists = false;
-
-				// check to see if any of our interfaces can fullfill requested ones
-				Set<String> myInterfaces = runtime.typeToInterface.get(type);
-				if (myInterfaces != null) {
-					for (String inter : myInterfaces) {
-						if (runtime.interfaceToNames.containsKey(inter)) {
-							runtime.interfaceToNames.get(inter).remove(name);
-							updatedServiceLists = true;
-						}
-					}
-				}
-
-				if (updatedServiceLists) {
-					runtime.invoke("publishInterfaceToNames");
-				}
-
-			}
-
-			// FIXME - release autostarted peers ?
-
-			// last step - remove from registry by making new registry
-			// thread safe way
-			Map<String, ServiceInterface> removedService = new TreeMap<>();
-			for (String key : registry.keySet()) {
-				if (!name.equals(key)) {
-					removedService.put(key, registry.get(key));
-				}
-			}
-			registry = removedService;
-
-			// and config
-			RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
-			if (c != null) {
-				c.remove(CodecUtils.getShortName(name));
-			}
-
-			log.info("released {}", name);
-		}
-	}
-
-	/**
-	 * Get all remote services.
-	 * 
-	 * @return List of remote services as proxies
-	 */
-	public List<ServiceInterface> getRemoteServices() {
-		return getRemoteServices(null);
-	}
-
-	/**
-	 * Get remote services associated with the MRL instance that has the given ID.
-	 * 
-	 * @param id The id of the target MRL instance
-	 * @return A list of services running on the target instance
-	 */
-	public List<ServiceInterface> getRemoteServices(String id) {
-		List<ServiceInterface> list = new ArrayList<>();
-		for (String serviceName : registry.keySet()) {
-			if (serviceName.contains("@")) {
-				String sid = serviceName.substring(serviceName.indexOf("@") + 1);
-				if (id == null || sid.equals(id)) {
-					list.add(registry.get(serviceName));
-				}
-			}
-		}
-		return list;
-	}
-
-	/**
-	 * Releases all local services including Runtime asynchronously.
-	 *
-	 * @see #releaseAll(boolean, boolean)
-	 */
-	public static void releaseAll() {
-		releaseAll(true, false);
-	}
-
-	/**
-	 * This does not EXIT(1) !!! releasing just releases all services
-	 *
-	 * FIXME FIXME FIXME - just call release on each - possibly saving runtime for
-	 * last .. send prepareForRelease before releasing
-	 *
-	 * release all local services
-	 *
-	 * FIXME - there "should" be an order to releasing the correct way would be to
-	 * save the Runtime for last and broadcast all the services being released
-	 *
-	 * FIXME - send SHUTDOWN event to all running services with a timeout period -
-	 * end with System.exit() FIXME normalize with releaseAllLocal and
-	 * releaseAllExcept
-	 *
-	 * local only? YES !!! LOCAL ONLY !!
-	 * 
-	 * @param releaseRuntime Whether the Runtime should also be released
-	 */
-	public static void releaseAll(boolean releaseRuntime, boolean block) {
-		// a command thread is issuing this command is most likely
-		// tied to one of the services being removed
-		// therefore this needs to happen asynchronously otherwise
-		// the thread that issued the command will try to destroy/release itself
-		// which almost always causes a deadlock
-		log.debug("releaseAll");
-
-		if (block) {
-			processRelease(releaseRuntime);
-			ConfigUtils.reset();
-		} else {
-
-			new Thread() {
-				@Override
-				public void run() {
-					processRelease(releaseRuntime);
-					ConfigUtils.reset();
-				}
-			}.start();
-
-		}
-	}
-
-	/**
-	 * Releases all threads and can be executed in a separate thread.
-	 *
-	 * @param releaseRuntime Whether the Runtime should also be released
-	 */
-	static private void processRelease(boolean releaseRuntime) {
-		synchronized (processLock) {
-			// reverse release to order of creation
-			Collection<ServiceInterface> local = getLocalServices().values();
-			List<ServiceInterface> ordered = new ArrayList<>(local);
-			ordered.removeIf(Objects::isNull);
-			Collections.sort(ordered);
-			Collections.reverse(ordered);
-
-			for (ServiceInterface sw : ordered) {
-
-				// no longer needed now - runtime "should be" guaranteed to be last
-				if (sw == Runtime.getInstance()) {
-					// skipping runtime
-					continue;
-				}
-
-				log.info("releasing service {}", sw.getName());
-
-				try {
-					sw.releaseService();
-				} catch (Exception e) {
-					if (runtime != null) {
-						runtime.error("%s threw while releasing", e);
-					}
-					log.error("release", e);
-				}
-			}
-
-			// clean up remote ... the contract should
-			// probably be just remove their references - do not
-			// ask for them to be released remotely ..
-			// in thread safe way
-
-			if (releaseRuntime) {
-				if (runtime != null) {
-					runtime.releaseService();
-				}
-				synchronized (INSTANCE_LOCK) {
-					runtime = null;
-				}
-			} else {
-				// put runtime in new registry
-				Runtime.getInstance();
-				registry = new TreeMap<>();
-				registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
-			}
-		}
-	}
-
-	/**
-	 * Shuts down this instance after the given number of seconds.
-	 *
-	 * @param seconds sets task to shutdown in (n) seconds
-	 */
-	// Why is this using the wrapper type? Null can be passed in and cause NPE
-	public static void shutdown(Integer seconds) {
-		log.info("shutting down in {} seconds", seconds);
-		if (seconds > 0) {
-			runtime.addTaskOneShot(seconds * 1000, "shutdown", (Object[]) null);
-			runtime.invoke("publishShutdown", seconds);
-		} else {
-			shutdown();
-		}
-	}
-
-	/**
-	 * shutdown terminates the currently running Java virtual machine by initiating
-	 * its shutdown sequence. This method never returns normally. The argument
-	 * serves as a status code; by convention, a nonzero status code indicates
-	 * abnormal termination
-	 *
-	 */
-	public static void shutdown() {
-		try {
-			log.info("myrobotlab shutting down");
-
-			if (runtime != null) {
-				log.info("stopping interactive mode");
-				runtime.stopInteractiveMode();
-			}
-
-			log.info("pre shutdown on all services");
-			for (ServiceInterface service : getServices()) {
-				service.preShutdown();
-			}
-
-			log.info("releasing all");
-
-			// release
-			releaseAll();
-		} catch (Exception e) {
-			log.error("something threw - continuing to shutdown", e);
-		}
-
-		// calling System.exit(0) before some specialized threads
-		// are completed will actually end up in a deadlock
-		Service.sleep(1000);
-		System.exit(0);
-	}
-
-	public Integer publishShutdown(Integer seconds) {
-		return seconds;
-	}
-
-	/**
-	 * publish the folders of the parent directory of configPath if the configPath
-	 * is null then publish directory names of data/config
-	 *
-	 * @return list of configs
-	 */
-	public List<String> publishConfigList() {
-		configList = new ArrayList<>();
-
-		File configDirFile = new File(ROOT_CONFIG_DIR);
-		if (!configDirFile.exists() || !configDirFile.isDirectory()) {
-			error("%s config root does not exist", configDirFile.getAbsolutePath());
-			return configList;
-		}
-
-		File[] files = configDirFile.listFiles();
-		if (files == null) {
-			// We checked for if directory earlier, so can only be null for IO error
-			error("IO error occurred while listing config directory files");
-			return configList;
-		}
-		for (File file : files) {
-			String n = file.getName();
-
-			if (!file.isDirectory() || file.isHidden()) {
-				log.info("ignoring {} expecting directory not file", n);
-				continue;
-			}
-
-			configList.add(file.getName());
-		}
-		Collections.sort(configList);
-		return configList;
-	}
-
-	/**
-	 * Releases all local services except the services whose names are in the given
-	 * set
-	 * 
-	 * @param saveMe The set of services that should not be released
-	 */
-	public static void releaseAllServicesExcept(HashSet<String> saveMe) {
-		log.info("releaseAllServicesExcept");
-		List<ServiceInterface> list = Runtime.getServices();
-		for (ServiceInterface si : list) {
-			if (saveMe != null && saveMe.contains(si.getName())) {
-				log.info("leaving {}", si.getName());
-			} else {
-				si.releaseService();
-			}
-		}
-	}
-
-	/**
-	 * Release a specific service. Releasing shuts down the service and removes it
-	 * from registries.
-	 *
-	 * @param fullName full name The service to be released
-	 *
-	 */
-	static public void release(String fullName) {
-		releaseService(fullName);
-	}
-
-	/**
-	 * Disconnect from remote process. FIXME - not implemented
-	 * 
-	 * @throws IOException Unknown
-	 */
-	// FIXME - implement ! also implement the callback events .. onDisconnect
-	public void disconnect() throws IOException {
-		// connect("admin", "ws://localhost:8887/api/messages");
-		log.info("disconnect");
-	}
-
-	/**
-	 * FIXME - can this be renamed back to attach ? jump to another process using
-	 * the cli
-	 *
-	 * @param id instance id.
-	 * @return string
-	 *
-	 */
-	// FIXME - remove - the way to 'jump' is just to change
-	// context to the correct mrl id e.g. cd /runtime@remote07
-	public String jump(String id) {
-		Connection c = getRoute(stdCliUuid);
-		if (c != null && c.get("cli") != null) {
-			((InProcessCli) c.get("cli")).setRemote(id);
-		} else {
-			log.error("connection or cli is null for uuid {}", stdCliUuid);
-		}
-
-		return id;
-	}
-
-	/**
-	 * Reconnects {@link #cli} to this process.
-	 * 
-	 * @return The id of this instance
-	 */
-	// FIXME - remove ?!?!!?
-	public String exit() {
-		Connection c = getConnection(stdCliUuid);
-		if (c != null && c.get("cli") != null) {
-			((InProcessCli) c.get("cli")).setRemote(getId());
-		}
-		return getId();
-	}
-
-	/**
-	 * Send a command to the {@link InProcessCli}.
-	 *
-	 * @param srcFullName Unknown
-	 * @param cmd         The command to execute
-	 */
-	public void sendToCli(String srcFullName, String cmd) {
-		Connection c = getConnection(stdCliUuid);
-		if (c == null || c.get("cli") == null) {
-			log.info("starting interactive mode");
-			startInteractiveMode();
-			sleep(1000);
-		}
-		c = getConnection(stdCliUuid);
-		if (c != null && c.get("cli") != null) {
-			((InProcessCli) c.get("cli")).process(srcFullName, cmd);
-		} else {
-			log.error("could not start interactive mode");
-		}
-	}
-
-	/**
-	 * Connect to the MRL instance at the given URL, auto-reconnecting if specified
-	 * and the connection drops.
-	 *
-	 * FIXME implement autoReconnect
-	 *
-	 * @param url           The URL to connect to
-	 * @param autoReconnect Whether the connection should be re-established if it is
-	 *                      dropped
-	 */
-	// FIXME - implement
-	public void connect(String url, boolean autoReconnect) {
-		if (!autoReconnect) {
-			connect(url);
-		} else {
-			addTask(1000, "checkConnections");
-		}
-	}
-
-	// FIXME - implement
-	public void checkConnections() {
-		for (Connection connection : connections.values()) {
-			if (connection.containsKey("url")) {
-				/*
-				 * FIXME - check on "STATE" ... means we support disconnected connections .. if
-				 * (connection.get("url").toString().equals(url)) { // already connected
-				 * continue; }
-				 */
-			}
-		}
-		// could not find our connection for this "id" - need to reconnect
-		// connect(url);
-	}
-
-	// FIXME -
-	// step 1 - first bind the uuids (1 local and 1 remote)
-	// step 2 - Clients will contain attribute
-	// FIXME - RETRIES TIMEOUTS OTHER COMPLEXITIES
-	// blocking connect - consider a non-blocking thread connect ... e.g.
-	// autoConnect
-
-	/**
-	 * Connect to the MRL instance at the given URL
-	 * 
-	 * @param url Where the MRL instance being connected to is located
-	 */
-	@Override
-	public void connect(String url) {
-		try {
-
-			// TODO - do auth, ssl and unit tests for them
-			// TODO - get session id
-			// request default describe - on describe do registrations .. zzz
-
-			// standardize request - TODO check for ws wss not http https
-			if (!url.contains("api/messages")) {
-				url += "/api/messages";
-			}
-
-			if (!url.contains("id=")) {
-				url += "?id=" + getId();
-			}
-
-			WsClient client2 = new WsClient();
-			client2.connect(this, url);
-
-			// URI uri = new URI(url);
-			// adding "id" as full url :P ... because we don't know it !!!
-			Connection connection = new Connection(client2.getId(), getId(), getFullName());
-
-			// connection specific
-			connection.put("c-type", "Runtime");
-			// attributes.put("c-endpoint", endpoint);
-			connection.put("c-client", client2);
-
-			// cli specific
-			connection.put("cwd", "/");
-			connection.put("url", url);
-			connection.put("uri", url); // not really correct
-			connection.put("user", "root");
-			connection.put("host", "local");
-
-			// addendum
-			connection.put("User-Agent", "runtime-client");
-
-			addConnection(client2.getId(), url, connection);
-
-			// direct send - may not have and "id" so it will be too runtime vs
-			// runtime@{id}
-			// subscribe to "describe"
-			MRLListener listener = new MRLListener("describe", getFullName(), "onDescribe");
-			Message msg = Message.createMessage(getFullName(), "runtime", "addListener", listener);
-			client2.send(CodecUtils.toJsonMsg(msg));
-
-			// send describe
-			client2.send(CodecUtils.toJsonMsg(getDescribeMsg(null)));
-
-		} catch (Exception e) {
-			log.error("connect to {} giving up {}", url, e.getMessage());
-		}
-	}
-
-	/**
-	 * FIXME - this is a gateway callback - probably should be in the gateway
-	 * interface - this is a "specific" gateway that supports typeless json or
-	 * websockets
-	 * <p>
-	 * FIXME - decoding should be done at the Connection ! - this should be
-	 * onRemoteMessage(msg) !
-	 * <p>
-	 * callback - from clientRemote - all client connections will recieve here TODO
-	 * - get clients directional api - an api per direction incoming and outgoing
-	 *
-	 * @param uuid - connection for incoming data
-	 * @param data Incoming message in JSON String form
-	 */
-	@Override // uuid
-	public void onRemoteMessage(String uuid, String data) {
-		try {
-
-			// log.debug("connection {} responded with {}", uuid, data);
-			// get api - decode msg - process it
-			Connection connection = getConnection(uuid);
-			if (connection == null) {
-				error("no connection with uuid %s", uuid);
-				return;
-			}
-
-			if (log.isDebugEnabled()) {
-				log.debug("data - [{}]", data);
-			}
-
-			// decoding message envelope
-			Message msg = CodecUtils.fromJson(data, Message.class);
-			log.info("==> {} --> {}.{}", msg.sender, msg.name, msg.method);
-			msg.setProperty("uuid", uuid); // Properties ???? REMOVE ???
-
-			if (msg.containsHop(getId())) {
-				log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{} | {}", getName(),
-						msg.sender, msg.name, msg.method, msg.getHops());
-				return;
-			}
-
-			addRoute(msg.getSrcId(), uuid, 10);
-
-			// add our id - we don't want to see it again
-			msg.addHop(getId());
-
-			Object ret = null;
-
-			// FIXME - see if same code block exists in WebGui .. normalize
-			if (isLocal(msg)) {
-
-				// log.info("--> {}.{} from {}", msg.name, msg.method, msg.sender);
-
-				String serviceName = msg.getName();
-				// to decode fully we need class name, method name, and an array of json
-				// encoded parameters
-				MethodCache cache = MethodCache.getInstance();
-				Class<?> clazz = Runtime.getClass(serviceName);
-				if (clazz == null) {
-					log.error("local msg but no Class for requested service {}", serviceName);
-					return;
-				}
-				Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, msg.data);
-
-				Method method = cache.getMethod(clazz, msg.method, params);
-				ServiceInterface si = Runtime.getService(serviceName);
-				if (method == null) {
-					log.error("cannot find {}", cache.makeKey(clazz, msg.method, cache.getParamTypes(params)));
-					return;
-				}
-				if (si == null) {
-					log.error("si null for serviceName {}", serviceName);
-					return;
-				}
-
-				ret = method.invoke(si, params);
-
-				// propagate return data to subscribers
-				si.out(msg.method, ret);
-
-			} else {
-				log.info("GATEWAY {} RELAY {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
-				send(msg);
-			}
-
-		} catch (Exception e) {
-			log.error("processing msg threw", e);
-		}
-	}
-
-	/**
-	 * Add a route to the route table
-	 *
-	 * @param remoteId Id of the remote instance
-	 * @param uuid     Unknown
-	 * @param metric   Unknown
-	 * @see RouteTable#addRoute(String, String, int)
-	 */
-	public void addRoute(String remoteId, String uuid, int metric) {
-		routeTable.addRoute(remoteId, uuid, metric);
-	}
-
-	/**
-	 * Start Runtime with the specified config
-	 *
-	 * @param configName The name of the config file
-	 */
-	static public void startConfig(String configName) {
-		setConfig(configName);
-		Runtime runtime = Runtime.getInstance();
-		runtime.processingConfig = true; // multiple inbox threads not available
-		runtime.invoke("publishConfigStarted", configName);
-		RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {
-		});
-		if (rtConfig == null) {
-			runtime.error("cannot find %s%s%s", runtime.getConfigName(), fs, "runtime.yml");
-			return;
-		}
-
-		runtime.apply(rtConfig);
-
-		Plan plan = new Plan("runtime");
-		// for every service listed in runtime registry - load it
-		// FIXME - regex match on filesystem matches on *.yml
-		for (String service : rtConfig.getRegistry()) {
-
-			if ("runtime".equals(service) || Runtime.isStarted(service)) {
-				continue;
-			}
-
-			// has to be loaded
-			File file = new File(Runtime.ROOT_CONFIG_DIR + fs + runtime.getConfigName() + fs + service + ".yml");
-			if (!file.exists()) {
-				runtime.error("cannot read file %s - skipping", file.getPath());
-				continue;
-			}
-
-			ServiceConfig sc = runtime.readServiceConfig(runtime.getConfigName(), service);
-			try {
-				if (sc == null) {
-					continue;
-				}
-				runtime.loadService(plan, service, sc.type, true, 0);
-			} catch (Exception e) {
-				runtime.error(e);
-			}
-		}
-
-		// for all newly created services start them
-		Map<String, ServiceInterface> created = Runtime.createServicesFromPlan(plan, null, null);
-		for (ServiceInterface si : created.values()) {
-			si.startService();
-		}
-
-		runtime.processingConfig = false; // multiple inbox threads not available
-		runtime.invoke("publishConfigFinished", configName);
-
-	}
-
-	public String publishConfigStarted(String configName) {
-		log.info("publishConfigStarted {}", configName);
-		// Make Note: done inline, because the thread actually doing the config
-		// processing
-		// would need to be finished with it before this thread could be invoked
-		// if multiple inbox threads were available then this would be possible
-		// processingConfig = true;
-		return configName;
-	}
-
-	public String publishConfigFinished(String configName) {
-		log.info("publishConfigFinished {}", configName);
-		// Make Note: done inline, because the thread actually doing the config
-		// processing
-		// would need to be finished with it before this thread could be invoked
-		// if multiple inbox threads were available then this would be possible
-		// processingConfig = false;
-		return configName;
-	}
-
-	/**
-	 * Start a service of the specified type as the specified name.
-	 *
-	 * @param name The name of the new service
-	 * @param type The type of the new service
-	 * @return The started service
-	 */
-	static public ServiceInterface start(String name, String type) {
-		synchronized (processLock) {
-			try {
-
-				ServiceInterface requestedService = Runtime.getService(name);
-				if (requestedService != null) {
-					log.info("requested service already exists");
-					if (requestedService.isRunning()) {
-						log.info("requested service already running");
-					} else {
-						requestedService.startService();
-					}
-					return requestedService;
-				}
-
-				Plan plan = Runtime.load(name, type);
-
-				Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
-
-				if (services == null) {
-					Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration",
-							name, type);
-					return null;
-				}
-
-				requestedService = Runtime.getService(name);
-
-				// FIXME - does some order need to be maintained e.g. all children
-				// before
-				// parent
-				// breadth first, depth first, external order ordinal ?
-				for (ServiceInterface service : services.values()) {
-					if (service.getName().equals(name)) {
-						continue;
-					}
-					if (!Runtime.isStarted(service.getName())) {
-						service.startService();
-					}
-				}
-
-				if (requestedService == null) {
-					Runtime.getInstance().error("could not start %s of type %s", name, type);
-					return null;
-				}
-
-				// getConfig() was problematic here for JMonkeyEngine
-				ServiceConfig sc = requestedService.getConfig();
-				// Map<String, Peer> peers = sc.getPeers();
-				// if (peers != null) {
-				// for (String p : peers.keySet()) {
-				// Peer peer = peers.get(p);
-				// log.info("peer {}", peer);
-				// }
-				// }
-				// recursive - start peers of peers of peers ...
-				Map<String, Peer> subPeers = sc.getPeers();
-				if (sc != null && subPeers != null) {
-					for (String subPeerKey : subPeers.keySet()) {
-						// IF AUTOSTART !!!
-						Peer subPeer = subPeers.get(subPeerKey);
-						if (subPeer.autoStart) {
-							Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
-						}
-					}
-				}
-
-				requestedService.startService();
-				return requestedService;
-			} catch (Exception e) {
-				runtime.error(e);
-			}
-			return null;
-		}
-	}
-
-	/**
-	 * single parameter name info supplied - potentially all information regarding
-	 * this service could be found in on the filesystem or in the plan
-	 * 
-	 * @param name
-	 * @return
-	 */
-	static public ServiceInterface start(String name) {
-		synchronized (processLock) {
-			if (Runtime.getService(name) != null) {
-				// already exists
-				ServiceInterface si = Runtime.getService(name);
-				if (!si.isRunning()) {
-					si.startService();
-				}
-				return si;
-			}
-			Plan plan = Runtime.load(name, null);
-			Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
-			// FIXME - order ?
-			for (ServiceInterface service : services.values()) {
-				service.startService();
-			}
-			return Runtime.getService(name);
-		}
-	}
-
-	public static Plan load(String name, String type) {
-		synchronized (processLock) {
-			try {
-				Runtime runtime = Runtime.getInstance();
-				return runtime.loadService(new Plan("runtime"), name, type, true, 0);
-			} catch (IOException e) {
-				runtime.error(e);
-			}
-			return null;
-		}
-	}
-
-	/**
-	 * Construct a new Runtime with the given name and ID. The name should always be
-	 * "runtime" as parts of interprocess communication assume it to be so.
-	 *
-	 * TODO Check if there's a way to remove the assumptions about Runtime's name
-	 * 
-	 * @param n  Name of the runtime. Should always be {@code "runtime"}
-	 * @param id The ID of the instance this runtime belongs to.
-	 */
-	public Runtime(String n, String id) {
-		super(n, id);
-
-		// because you need to start with something ...
-		config = new RuntimeConfig();
-
-		repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
-
-		/**
-		 * This is used to run through all the possible services and determine if they
-		 * have any missing dependencies. If they do not they become "installed". The
-		 * installed flag makes the gui do a crossout when a service type is selected.
-		 */
-		for (MetaData metaData : serviceData.getServiceTypes()) {
-			Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
-			if (deps.size() == 0) {
-				metaData.installed = true;
-			} else {
-				log.info("{} not installed", metaData.getSimpleName());
-			}
-		}
-
-		setLocale(Locale.getDefault().getTag());
-		locales = Locale.getDefaults();
-
-		if (runtime.platform == null) {
-			runtime.platform = Platform.getLocalInstance();
-		}
-
-		// setting the id and the platform
-		platform = Platform.getLocalInstance();
-
-		String libararyPath = System.getProperty("java.library.path");
-		String userDir = System.getProperty("user.dir");
-		String userHome = System.getProperty("user.home");
-
-		// initialize the config list
-		publishConfigList();
-
-		// TODO this should be a single log statement
-		// http://developer.android.com/reference/java/lang/System.html
-
-		String format = "yyyy/MM/dd HH:mm:ss";
-		SimpleDateFormat sdf = new SimpleDateFormat(format);
-		SimpleDateFormat gmtf = new SimpleDateFormat(format);
-		gmtf.setTimeZone(TimeZone.getTimeZone("UTC"));
-		log.info("============== args begin ==============");
-		StringBuffer sb = new StringBuffer();
-
-		jvmArgs = getJvmArgs();
-		if (globalArgs != null) {
-			for (int i = 0; i < globalArgs.length; ++i) {
-				sb.append(globalArgs[i]);
-			}
-		}
-		if (jvmArgs != null) {
-			log.info("jvmArgs {}", Arrays.toString(jvmArgs.toArray()));
-		}
-		log.info("file.encoding {}", System.getProperty("file.encoding"));
-		log.info("args {}", Arrays.toString(globalArgs));
-
-		log.info("============== args end ==============");
-
-		log.info("============== env begin ==============");
-
-		Map<String, String> env = System.getenv();
-		if (env.containsKey("PATH")) {
-			log.info("PATH={}", env.get("PATH"));
-		} else {
-			log.info("PATH not defined");
-		}
-		if (env.containsKey("JAVA_HOME")) {
-			log.info("JAVA_HOME={}", env.get("JAVA_HOME"));
-		} else {
-			log.info("JAVA_HOME not defined");
-		}
-
-		// also look at bitness detection in framework.Platform
-		String procArch = env.get("PROCESSOR_ARCHITECTURE");
-		String procArchWow64 = env.get("PROCESSOR_ARCHITEW6432");
-		if (procArch != null) {
-			log.info("PROCESSOR_ARCHITECTURE={}", procArch);
-		} else {
-			log.info("PROCESSOR_ARCHITECTURE not defined");
-		}
-		if (procArchWow64 != null) {
-			log.info("PROCESSOR_ARCHITEW6432={}", procArchWow64);
-		} else {
-			log.info("PROCESSOR_ARCHITEW6432 not defined");
-		}
-		log.info("============== env end ==============");
-
-		log.info("============== platform ==============");
-		long startTime = platform.getStartTime().getTime();
-		log.info("{} - GMT - {}", sdf.format(startTime), gmtf.format(startTime));
-		log.info("pid {}", platform.getPid());
-		log.info("hostname {}", platform.getHostname());
-		log.info("ivy [runtime,{}.{}.{}]", platform.getArch(), platform.getJvmBitness(), platform.getOS());
-		log.info("version {} branch {} commit {} build {}", platform.getVersion(), platform.getBranch(),
-				platform.getCommit(), platform.getBuild());
-		System.out.println(String.format("version %s branch %s commit %s build %s", platform.getVersion(),
-				platform.getBranch(), platform.getCommit(), platform.getBuild()));
-		log.info("platform manifest {}", Platform.getManifest());
-		log.info("platform [{}}]", platform);
-		log.info("version [{}]", platform.getVersion());
-		log.info("root [{}]", FileIO.getRoot());
-		log.info("cfg dir [{}]", FileIO.getCfgDir());
-		log.info("sun.arch.data.model [{}]", System.getProperty("sun.arch.data.model"));
-
-		log.info("============== non-normalized ==============");
-		log.info("os.name [{}] getOS [{}]", System.getProperty("os.name"), platform.getOS());
-		log.info("os.arch [{}] getArch [{}]", System.getProperty("os.arch"), platform.getArch());
-		log.info("os.version [{}]", System.getProperty("os.version"));
-
-		log.info("java.vm.name [{}]", System.getProperty("java.vm.name"));
-		log.info("java.vm.vendor [{}]", System.getProperty("java.vm.vendor"));
-		log.info("java.specification.version [{}]", System.getProperty("java.specification.version"));
-
-		String vmVersion = System.getProperty("java.specification.version");
-		vmVersion = "11";
-		if ("1.8".equals(vmVersion)) {
-			error("Unsupported Java %s - please remove version and install Java 1.8", vmVersion);
-		}
-
-		// test ( force encoding )
-		// System.setProperty("file.encoding","UTF-8" );
-		log.info("file.encoding [{}]", System.getProperty("file.encoding"));
-		log.info("Charset.defaultCharset() [{}]", Charset.defaultCharset());
-		log.info("user.language [{}]", System.getProperty("user.language"));
-		log.info("user.country [{}]", System.getProperty("user.country"));
-		log.info("user.variant [{}]", System.getProperty("user.variant"));
-
-		// System.getProperty("pi4j.armhf")
-
-		log.info("java.home [{}]", System.getProperty("java.home"));
-		log.debug("java.class.path [{}]", System.getProperty("java.class.path"));
-		log.info("java.library.path [{}]", libararyPath);
-		log.info("user.dir [{}]", userDir);
-
-		log.info("user.home [{}]", userHome);
-		log.info("total mem [{}] Mb", Runtime.getTotalMemory() / 1048576);
-		log.info("total free [{}] Mb", Runtime.getFreeMemory() / 1048576);
-		// Access restriction - log.info("total physical mem [{}] Mb",
-		// Runtime.getTotalPhysicalMemory() / 1048576);
-
-		if (platform.isWindows()) {
-			log.info("guessed os bitness [{}]", platform.getOsBitness());
-			// try to compare os bitness with jvm bitness
-			if (platform.getOsBitness() != platform.getJvmBitness()) {
-				log.warn("detected possible bitness mismatch between os & jvm");
-			}
-		}
-
-		log.info("getting local repo");
-
-		if (repo != null)/* transient */ {
-			repo.addStatusPublisher(this);
-		}
-	}
-
-	/**
-	 * Get the process ID of the current JVM.
-	 *
-	 * @return The process ID.
-	 * @see Platform#getPid()
-	 */
-	public String getPid() {
-		return Platform.getLocalInstance().getPid();
-	}
-
-	public String publishDefaultRoute(String defaultRoute) {
-		return defaultRoute;
-	}
-
-	/**
-	 * Get the hostname of the computer this instance is running on.
-	 * 
-	 * @return The computer's hostname
-	 * @see Platform#getHostname()
-	 */
-	public String getHostname() {
-		return Platform.getLocalInstance().getHostname();
-	}
-
-	/**
-	 * publishing event - since checkForUpdates may take a while
-	 */
-	public void checkingForUpdates() {
-		log.info("checking for updates");
-	}
-
-	/**
-	 * Read an entire input stream as a string and return it. If the input stream
-	 * does not have any more tokens, returns an empty string instead.
-	 *
-	 * @param is The input stream to read from
-	 * @return The entire input stream read as a string
-	 */
-	static public String getInputAsString(InputStream is) {
-		try (java.util.Scanner s = new java.util.Scanner(is)) {
-			return s.useDelimiter("\\A").hasNext() ? s.next() : "";
-		}
-	}
-
-	/**
-	 * list the contents of the current working directory
-	 *
-	 * @return object
-	 */
-	public Object ls() {
-		return ls(null, null);
-	}
-
-	/**
-	 * List the contents of an absolute path.
-	 *
-	 * @param path The path to list
-	 * @return The contents of the directory
-	 */
-	public Object ls(String path) {
-		return ls(null, path);
-	}
-
-	/**
-	 * list the contents of a specific path
-	 * <p>
-	 * </p>
-	 * TODO It looks like this only returns Object because it wants to return either
-	 * a String array or a method entry list. It would probably be best to just
-	 * convert the method entry list to a string array using streams and change the
-	 * signature to match.
-	 *
-	 * @param contextPath c
-	 * @param path        p
-	 * @return object
-	 *
-	 */
-	public Object ls(String contextPath, String path) {
-		String absPath = null;
-
-		if (contextPath != null) {
-			path = contextPath + path;
-		}
-
-		if (path == null) {
-			path = "/";
-		}
-
-		// ALL SHOULD BE ABSOLUTE PATH AT THIS POINT
-		// IE STARTING WITH /
-
-		if (!path.startsWith("/")) {
-			path = "/" + path;
-		}
-
-		absPath = path;
-
-		String[] parts = absPath.split("/");
-
-		String ret = null;
-		if (absPath.equals("/")) {
-			return Runtime.getServiceNames();
-		} else if (parts.length == 2 && !absPath.endsWith("/")) {
-			return Runtime.getService(parts[1]);
-		} else if (parts.length == 2 && absPath.endsWith("/")) {
-			ServiceInterface si = Runtime.getService(parts[1]);
-			if (si == null) {
-				return null;
-			}
-			return si.getDeclaredMethodNames();
-			/*
-			 * } else if (parts.length == 3 && !absPath.endsWith("/")) { // execute 0
-			 * parameter function ??? return Runtime.getService(parts[1]);
-			 */
-		} else if (parts.length == 3) {
-			ServiceInterface si = Runtime.getService(parts[1]);
-			MethodCache cache = MethodCache.getInstance();
-			List<MethodEntry> me = cache.query(si.getTypeKey(), parts[2]);
-			return me; // si.getMethodMap().get(parts[2]);
-		}
-		return ret;
-	}
-
-	/**
-	 * serviceName at id
-	 *
-	 * @return runtime name with instance id.
-	 *
-	 */
-	public String whoami() {
-		return "runtime@" + getId();
-	}
-
-	// end cli commands ----
-
-	// ---------- Java Runtime wrapper functions begin --------
-	/**
-	 * Executes the specified command and arguments in a separate process. Returns
-	 * the exit value for the subprocess.
-	 *
-	 * @param program The name of or path to an executable program. If given a name,
-	 *                the program must be on the system PATH.
-	 * @return The exit value of the subprocess
-	 */
-	static public String exec(String program) {
-		return execute(program, null, null, null, null);
-	}
-
-	/*
-	 * FIXME - see if this is used anymore publishing point of Ivy sub system -
-	 * sends event failedDependency when the retrieve report for a Service fails
-	 */
-	@Deprecated /* remove */
-	public String failedDependency(String dep) {
-		return dep;
-	}
-
-	public static Platform getPlatform() {
-		return getInstance().platform;
-	}
-
-	// FIXME - should be removed - use Platform.getLocalInstance().is64bit()
-	@Deprecated
-	public boolean is64bit() {
-		return getInstance().platform.getJvmBitness() == 64;
-	}
-
-	public Repo getRepo() {
-		return repo;
-	}
-
-	/**
-	 * Returns an array of all the simple type names of all the possible services.
-	 * The data originates from the repo's serviceData.json file.
-	 * <p>
-	 * There is a local one distributed with the installation jar. When an "update"
-	 * is forced, MRL will try to download the latest copy from the repo.
-	 * <p>
-	 * The serviceData.json lists all service types, dependencies, categories and
-	 * other relevant information regarding service creation
-	 *
-	 * @return list of all service type names
-	 */
-	public String[] getServiceTypeNames() {
-		return getServiceTypeNames("all");
-	}
-
-	/**
-	 * getServiceTypeNames will publish service names based on some filter criteria
-	 *
-	 * @param filter f
-	 * @return array of service types
-	 *
-	 */
-	public String[] getServiceTypeNames(String filter) {
-		return serviceData.getServiceTypeNames(filter);
-	}
-
-	// FIXME THIS IS NOT NORMALIZED !!!
-
-	/**
-	 * Send the full log of the currently running MRL instance to the MyRobotLab
-	 * developers for help. The userID is the name of the MyRobotLab.org user
-	 * account
-	 * 
-	 * @param userId Name of the MRL website account to link the log to
-	 * @return Whether the log was sent successfully, info if yes and error if no.
-	 */
-	static public Status noWorky(String userId) {
-		Status status = null;
-		try {
-			String retStr = HttpRequest.postFile("http://noworky.myrobotlab.org/no-worky", userId, "file",
-					new File(LoggingFactory.getLogFileName()));
-			if (retStr.contains("Upload:")) {
-				log.info("noWorky successfully sent - our crack team of experts will check it out !");
-				status = Status.info("no worky sent");
-			} else {
-				status = Status.error("could not send");
-			}
-		} catch (Exception e) {
-			log.error("the noWorky didn't worky !");
-			status = Status.error(e);
-		}
-
-		// this makes the 'static' of this method pointless
-		// perhaps the webgui should invoke rather than call directly .. :P
-		Runtime.getInstance().invoke("publishNoWorky", status);
-		return status;
-	}
-
-	static public Status publishNoWorky(Status status) {
-		return status;
-	}
-
-	// FIXME - create interface for this
-	public String publishMessage(String msg) {
-		return msg;
-	}
-
-	@Override
-	@Deprecated /* use onResponse ??? */
-	public void onMessage(Message msg) {
-		// TODO: what do we do when we get a message?
-		log.info("onMessage()");
-	}
-
-	/**
-	 * Publishing point when a service was successfully registered locally -
-	 * regardless if the service is local or not.
-	 *
-	 * TODO - more business logic can be created here to limit broadcasting or
-	 * re-broadcasting published registrations
-	 *
-	 * @param registration - contains all the information need for a registration to
-	 *                     process
-	 */
-	@Override
-	public Registration registered(Registration registration) {
-		return registration;
-	}
-
-	/**
-	 * released event - when a service is successfully released from the registry
-	 * this event is triggered
-	 *
-	 */
-	@Override
-	public String released(String name) {
-		return name;
-	}
-
-	/**
-	 * A function for runtime to "save" a service - or if the service does not
-	 * exists save the "default" config of that type of service
-	 *
-	 * @param name name of service to export
-	 * @return true/false
-	 * @throws IOException boom
-	 *
-	 */
-	@Deprecated /* use save(name) */
-	public boolean export(String name /* , String type */) throws IOException {
-		return save(name);
-	}
-
-	public boolean save(String name /* , String type */) throws IOException {
-		ServiceInterface si = getService(name);
-		if (si != null) {
-			return si.save();
-		}
-		error("cannot save %s - does not exist", name);
-		return false;
-	}
-
-	/**
-	 * restart occurs after applying updates - user or config data needs to be
-	 * examined and see if its an appropriate time to restart - if it is the
-	 * spawnBootstrap method will be called and bootstrap.jar will go through its
-	 * sequence to update myrobotlab.jar
-	 */
-	public void restart() {
-		// to avoid deadlock of shutting down from external messages
-		// we spawn a kill thread
-		new Thread("kill-thread") {
-			@Override
-			public void run() {
-				try {
-
-					info("restarting");
-
-					// FIXME - should we save() load() ???
-					// export("last-restart");
-
-					// shutdown all services process - send ready to shutdown - ask back
-					// release all services
-					for (ServiceInterface service : getServices()) {
-						service.preShutdown();
-					}
-
-					// check if ready ???
-
-					// release all local services
-					releaseAll();
-
-					if (runtime != null) {
-						runtime.releaseService();
-					}
-
-					// make sure python is included
-					// options.services.add("python");
-					// options.services.add("Python");
-
-					// force invoke
-					// options.invoke = new String[] { "python", "execFile",
-					// "lastRestart.py" };
-
-					// create builder from Launcher daemonize ?
-					log.info("re launching with commands \n{}", CmdOptions.toString(options.getOutputCmd()));
-					ProcessBuilder pb = Launcher.createBuilder(options);
-
-					// fire it off
-					Process restarted = pb.start();
-					// it "better" not be a requirement that a process must consume its
-					// std streams
-					// "hopefully" - if the OS realizes the process is dead it moves the
-					// streams to /dev/null ?
-					// StreamGobbler gobbler = new
-					// StreamGobbler(String.format("%s-gobbler", getName()),
-					// restarted.getInputStream());
-					// gobbler.start();
-
-					// dramatic pause
-					sleep(2000);
-
-					// check if process exists
-					if (restarted.isAlive()) {
-						log.info("yay! we continue to live in future generations !");
-					} else {
-						log.error("omg! ... I killed all the services and now there is no offspring ! :(");
-					}
-					log.error("goodbye ...");
-					shutdown();
-				} catch (Exception e) {
-					log.error("shutdown threw", e);
-				}
-			}
-		}.start();
-	}
-
-	/**
-	 * Get the META-INF/MANIFEST.MF file from the myrobotlab.jar as String key-value
-	 * pairs.
-	 * 
-	 * @return key-value pairs contained in the manifest file
-	 * @see Platform#getManifest()
-	 */
-	static public Map<String, String> getManifest() {
-		return Platform.getManifest();
-	}
-
-	/**
-	 * Runtime's setLogLevel will set the root log level if its called from a
-	 * service - it will only set that Service type's log level
-	 *
-	 * @param level - DEBUG | INFO | WARN | ERROR
-	 * @return the level which was set
-	 */
-	static public String setLogLevel(String level) {
-		log.info("setLogLevel {}", level);
-		Logging logging = LoggingFactory.getInstance();
-		logging.setLevel(level);
-		log.info("setLogLevel {}", level);
-		return level;
-	}
-
-	/**
-	 * Get the log level of this MRL instance
-	 *
-	 * @return The log level as a String.
-	 * @see Logging#getLevel()
-	 */
-	static public String getLogLevel() {
-		Logging logging = LoggingFactory.getInstance();
-		return logging.getLevel();
-	}
-
-	/**
-	 * Set the file to output logs to. This will remove all previously-applied
-	 * appenders from the logging system.
-	 *
-	 * @param file The file to output logs to
-	 * @return file
-	 * @see Logging#removeAllAppenders()
-	 */
-	static public String setLogFile(String file) {
-		log.info("setLogFile {}", file);
-		Logging logging = LoggingFactory.getInstance();
-		logging.removeAllAppenders();
-		LoggingFactory.setLogFile(file);
-		logging.addAppender(AppenderType.FILE);
-		return file;
-	}
-
-	/**
-	 * Disables logging by removing all appenders. To re-enable call
-	 * {@link #setLogFile(String)} or add appenders.
-	 *
-	 * @see Logging#addAppender(String)
-	 */
-	static public void disableLogging() {
-		Logging logging = LoggingFactory.getInstance();
-		logging.removeAllAppenders();
-	}
-
-	/**
-	 * Stops all service-related running items. This releases the singleton
-	 * referenced by this class, but it does not guarantee that the old service will
-	 * be GC'd. FYI - if stopServices does not remove INSTANCE - it is not
-	 * re-entrant in junit tests
-	 */
-	@Override
-	public void releaseService() {
-		if (runtime != null) {
-			runtime.purgeTasks();
-			runtime.stopService();
-			runtime.stopInteractiveMode();
-			runtime.getRepo().removeStatusPublishers();
-			if (cli != null) {
-				cli.stop();
-			}
-			registry = new TreeMap<>();
-		}
-		synchronized (INSTANCE_LOCK) {
-			runtime = null;
-		}
-	}
-
-	/**
-	 * Close all connections using this runtime as the gateway. This includes both
-	 * inbound and outbound connections.
-	 */
-	public void closeConnections() {
-		for (Connection c : connections.values()) {
-			String gateway = c.getGateway();
-			if (getFullName().equals(gateway)) {
-				WsClient client = (WsClient) c.get("c-client");
-				client.close();
-			}
-		}
-	}
-
-	// FYI - the way to call "all" service methods !
-
-	/**
-	 * Clear all services' last error.
-	 * 
-	 * @see ServiceInterface#clearLastError()
-	 */
-	public void clearErrors() {
-		for (String serviceName : registry.keySet()) {
-			send(serviceName, "clearLastError");
-		}
-	}
-
-	/**
-	 * Check if any services have errors.
-	 *
-	 * @return Whether any service has an error
-	 * @see ServiceInterface#hasError()
-	 */
-	public static boolean hasErrors() {
-		for (ServiceInterface si : registry.values()) {
-			if (si.hasError()) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	/**
-	 * remove all subscriptions from all local Services
-	 */
-	static public void removeAllSubscriptions() {
-		for (ServiceInterface si : getLocalServices().values()) {
-			List<String> nlks = si.getNotifyListKeySet();
-			for (int i = 0; i < nlks.size(); ++i) {
-				si.getNotifyList().clear();
-			}
-		}
-	}
-
-	/**
-	 * Get recent errors from all local services.
-	 * 
-	 * @return A list of most recent service errors
-	 * @see ServiceInterface#getLastError()
-	 */
-	public static List<Status> getErrors() {
-		ArrayList<Status> stati = new ArrayList<Status>();
-		for (ServiceInterface si : getLocalServices().values()) {
-			Status status = si.getLastError();
-			if (status != null && status.isError()) {
-				log.info(status.toString());
-				stati.add(status);
-			}
-		}
-		return stati;
-	}
-
-	/**
-	 * Broadcast the states of all local services.
-	 */
-	public static void broadcastStates() {
-		for (ServiceInterface si : getLocalServices().values()) {
-			si.broadcastState();
-		}
-	}
-
-	/**
-	 * Get the Runtime singleton instance.
-	 * 
-	 * @return The singleton instance
-	 * @see #getInstance()
-	 */
-	public static Runtime get() {
-		return Runtime.getInstance();
-	}
-
-	/**
-	 * Execute an external program with arguments if specified. args must not be
-	 * null and the length must be greater than zero, the first element is the
-	 * program to be executed. If the program is just a name and not a path to the
-	 * executable then it must be on the operating system PATH.
-	 *
-	 * @see <a href=
-	 *      "https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them">
-	 *      What are PATH and other environment variables?</a>
-	 * @param args The program to be executed as the first element and the args to
-	 *             the program as the rest, if any
-	 * @return The program's stdout and stderr output
-	 */
-	static public String execute(String... args) {
-		if (args == null || args.length == 0) {
-			log.error("execute invalid number of args");
-			return null;
-		}
-		String program = args[0];
-		List<String> list = null;
-
-		if (args.length > 1) {
-			list = new ArrayList<String>();
-			for (int i = 1; i < args.length; ++i) {
-				list.add(args[i]);
-			}
-		}
-
-		return execute(program, list, null, null, true);
-	}
-
-	/**
-	 * Execute an external program with a list of arguments, a specified working
-	 * directory, any additional environment variables, and whether the execution
-	 * blocks.
-	 *
-	 * TODO Implement workingDir and block
-	 *
-	 * @param program       The program to be executed
-	 * @param args          Any arguments to the command
-	 * @param workingDir    The directory to execute the program in
-	 * @param additionalEnv Any additional environment variables
-	 * @param block         Whether this method blocks for the program to execute
-	 * @return The programs stderr and stdout output
-	 */
-
-	static public String execute(String program, List<String> args, String workingDir,
-			Map<String, String> additionalEnv, boolean block) {
-		log.debug("execToString(\"{} {}\")", program, args);
-
-		List<String> command = new ArrayList<>();
-		command.add(program);
-		if (args != null) {
-			command.addAll(args);
-		}
-
-		ProcessBuilder builder = new ProcessBuilder(command);
-		if (workingDir != null) {
-			builder.directory(new File(workingDir));
-		}
-
-		Map<String, String> environment = builder.environment();
-		if (additionalEnv != null) {
-			environment.putAll(additionalEnv);
-		}
-
-		StringBuilder outputBuilder = new StringBuilder();
-
-		try {
-			Process handle = builder.start();
-
-			InputStream stdErr = handle.getErrorStream();
-			InputStream stdOut = handle.getInputStream();
-
-			// Read the output streams in separate threads to avoid potential blocking
-			Thread stdErrThread = new Thread(() -> readStream(stdErr, outputBuilder));
-			stdErrThread.start();
-
-			Thread stdOutThread = new Thread(() -> readStream(stdOut, outputBuilder));
-			stdOutThread.start();
-
-			if (block) {
-				int exitValue = handle.waitFor();
-				outputBuilder.append("Exit Value: ").append(exitValue);
-				log.info("Command exited with exit value: {}", exitValue);
-			} else {
-				log.info("Command started");
-			}
-
-			return outputBuilder.toString();
-		} catch (IOException e) {
-			log.error("Error executing command", e);
-			return e.getMessage();
-		} catch (InterruptedException e) {
-			Thread.currentThread().interrupt();
-			log.error("Command execution interrupted", e);
-			return e.getMessage();
-		}
-	}
-
-	private static void readStream(InputStream inputStream, StringBuilder outputBuilder) {
-		try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
-			String line;
-			while ((line = reader.readLine()) != null) {
-				outputBuilder.append(line).append(System.lineSeparator());
-			}
-		} catch (IOException e) {
-			log.error("Error reading process output", e);
-		}
-	}
-
-	/**
-	 * Get the current battery level of the computer this MRL instance is running
-	 * on.
-	 *
-	 * @return The battery level as a double from 0.0 to 100.0, expressed as a
-	 *         percentage.
-	 */
-	public static Double getBatteryLevel() {
-		Platform platform = Platform.getLocalInstance();
-		Double r = 100.0;
-		try {
-			if (platform.isWindows()) {
-				// String ret = Runtime.execute("cmd.exe", "/C", "WMIC.exe", "PATH",
-				// "Win32_Battery", "Get", "EstimatedChargeRemaining");
-				String ret = Runtime.execute("WMIC.exe", "PATH", "Win32_Battery", "Get", "EstimatedChargeRemaining");
-				int pos0 = ret.indexOf("\n");
-				if (pos0 != -1) {
-					pos0 = pos0 + 1;
-					int pos1 = ret.indexOf("\n", pos0);
-					String dble = ret.substring(pos0, pos1).trim();
-					try {
-						r = Double.parseDouble(dble);
-					} catch (Exception e) {
-						log.error("no Battery detected by system");
-					}
-
-					return r;
-				}
-
-			} else if (platform.isLinux()) {
-				// TODO This is incorrect, will not work when unplugged
-				// and acpitool output is different than expected,
-				// at least on Ubuntu 22.04 - consider oshi library
-				String ret = Runtime.execute("acpi");
-				int pos0 = ret.indexOf("%");
-
-				if (pos0 != -1) {
-					int pos1 = ret.lastIndexOf(" ", pos0);
-					// int pos1 = ret.indexOf("%", pos0);
-					String dble = ret.substring(pos1, pos0).trim();
-					try {
-						r = Double.parseDouble(dble);
-					} catch (Exception e) {
-						log.error("no Battery detected by system");
-					}
-					return r;
-				}
-				log.info(ret);
-			} else if (platform.isMac()) {
-				String ret = Runtime.execute("pmset -g batt");
-				int pos0 = ret.indexOf("Battery-0");
-				if (pos0 != -1) {
-					pos0 = pos0 + 10;
-					int pos1 = ret.indexOf("%", pos0);
-					String dble = ret.substring(pos0, pos1).trim();
-					try {
-						r = Double.parseDouble(dble);
-					} catch (Exception e) {
-						log.error("no Battery detected by system");
-					}
-					return r;
-				}
-				log.info(ret);
-			}
-
-		} catch (Exception e) {
-			log.info("execToString threw", e);
-		}
-		return r;
-	}
-
-	/**
-	 * Get the local service data instance.
-	 * 
-	 * @return The local service data
-	 * @see ServiceData#getLocalInstance()
-	 */
-	public ServiceData getServiceData() {
-		return serviceData;
-	}
-
-	/**
-	 * Return supported system languages
-	 *
-	 * @return map of languages to locales
-	 */
-	public Map<String, Locale> getLanguages() {
-		return Locale.getAvailableLanguages();
-	}
-
-	/**
-	 * Get a map between locale IDs and the associated {@link Locale} instance.
-	 *
-	 * @return A map between IDs and instances.
-	 */
-	@Override
-	public Map<String, Locale> getLocales() {
-		return locales;
-	}
-
-	/**
-	 * Set the locales by passing a list of locale IDs.
-	 *
-	 * @param codes A list of locale IDs
-	 * @return A map between the IDs and the Locale instances.
-	 */
-	public Map<String, Locale> setLocales(String... codes) {
-		locales = Locale.getLocaleMap(codes);
-		return locales;
-	}
-
-	/**
-	 * @return get the Security singleton
-	 *
-	 *
-	 */
-	static public Security getSecurity() {
-		return Security.getInstance();
-	}
-
-	/**
-	 * Execute a program with arguments, if any. Wraps
-	 * {@link java.lang.Runtime#exec(String[])}.
-	 *
-	 * @param cmd A list with the program name as the first element and any
-	 *            arguments as the subsequent elements.
-	 * @return The Process spawned by the execution
-	 * @throws IOException if an I/O error occurs while spawning the process
-	 */
-	public static Process exec(String... cmd) throws IOException {
-		// FIXME - can't return a process - it will explode in serialization
-		// but we might want to keep it and put it on a transient map
-		log.info("Runtime exec {}", Arrays.toString(cmd));
-		Process p = java.lang.Runtime.getRuntime().exec(cmd);
-		return p;
-	}
-
-	/**
-	 * Get all the options passed on the command line when MyRobotLab is executed.
-	 *
-	 * @return The options that were passed on the command line
-	 */
-	public static CmdOptions getOptions() {
-		return options;
-	}
-
-	/**
-	 * TODO Unimplemented
-	 * 
-	 * @param sd ServiceData to use
-	 * @return sd
-	 */
-	public ServiceData setServiceTypes(ServiceData sd) {
-		return sd;
-	}
-
-	/**
-	 * FIXME - describe will have the capability to describe many aspects of a
-	 * running service. Default behavior will show a list of local names, but
-	 * depending on input criteria it should be possible to show * interfaces *
-	 * service data * service methods * details of a service method * help/javadoc
-	 * of a service method * list of other known instances * levels of detail, or
-	 * lists of fields to display * meaningful default
-	 *
-	 * FIXME - input parameters will need to change - at some point, a subscribe to
-	 * describe, and appropriate input parameters should replace the current
-	 * onRegistered system
-	 *
-	 * @param type       t
-	 * @param id         i
-	 * @param remoteUuid remote id
-	 * @return describe results
-	 *
-	 */
-	public DescribeResults describe(String type, String id, String remoteUuid) {
-		DescribeQuery query = new DescribeQuery(type, remoteUuid);
-		return describe(type, query);
-	}
-
-	/**
-	 * Get a default DescribeResults from this instance.
-	 *
-	 * @return A default description of this instance
-	 */
-	public DescribeResults describe() {
-		// default query
-		return describe("platform", null);
-	}
-
-	/**
-	 * Describe results returns the information of a "describe" which can be
-	 * detailed information regarding services, theire methods and input or output
-	 * types.
-	 * <p>
-	 * FIXME - describe(String[] filters) where filter can be name, type, local,
-	 * state, etc
-	 * <p>
-	 * FIXME uuid and query are unused
-	 *
-	 * @param uuid  u
-	 * @param query q
-	 * @return describe results
-	 *
-	 *
-	 *
-	 */
-	public DescribeResults describe(String uuid, DescribeQuery query) {
-
-		DescribeResults results = new DescribeResults();
-		results.setStatus(Status.success("Ahoy!"));
-
-		String fullname = null;
-
-		try {
-
-			results.setId(getId());
-			results.setPlatform(Platform.getLocalInstance());
-
-			// broadcast completed connection information
-			invoke("getConnections"); // FIXME - why isn't this done before ???
-
-			Set<String> set = registry.keySet();
-			String[] list = new String[set.size()];
-			set.toArray(list);
-
-			// TODO - filtering on what is broadcasted or re-broadcasted
-			for (int i = 0; i < list.length; ++i) {
-				fullname = list[i];
-				ServiceInterface si = registry.get(fullname);
-
-				Registration registration = new Registration(si);
-
-				results.addRegistration(registration);
-			}
-
-		} catch (Exception e) {
-			log.error("describe threw on {}", fullname, e);
-		}
-
-		return results;
-	}
-
-	/**
-	 * Describe results from remote query to describe
-	 *
-	 * @param results describe results
-	 *
-	 *
-	 */
-	public void onDescribe(DescribeResults results) {
-		List<Registration> reservations = results.getReservations();
-		if (reservations != null) {
-			for (Registration reservation : reservations) {
-				if ("runtime".equals(reservation.getName()) && !getId().equals(reservation.getId())) {
-					// If there's a reservation for a remote runtime, subscribe to its
-					// registered
-					// Maybe this should be done in register()?
-					subscribe(reservation.getFullName(), "registered");
-				}
-				register(reservation);
-			}
-		}
-
-	}
-
-	/**
-	 * IMPORTANT IMPORTANT IMPORTANT - Newly connected remote mrl processes blas a
-	 * list of registrations through onRegistered messages, for each service they
-	 * currently have in their registry. This process will send a list of
-	 * registrations to the newly connected remote process. If the "registered"
-	 * event is subscribed, any newly created service will be broadcasted thorough
-	 * this publishing point as well.
-	 *
-	 * TODO - write filtering, configuration, or security which affects what can be
-	 * registered
-	 *
-	 * Primarily, this is where new services are registered from remote systems
-	 *
-	 *
-	 */
-	public void onRegistered(Registration registration) {
-		try {
-			// check if registered ?
-
-			// TODO - filtering - include/exclude
-
-			String fullname = registration.getName() + "@" + registration.getId();
-			if (!registry.containsKey(fullname)) {
-				register(registration);
-				if (fullname.startsWith("runtime@")) {
-					// We want to TELL remote runtime if we have new registrations - we'll
-					// send them
-					// to it's runtime
-					// subscribe(fullname, "registered");
-					// subscribe(fullname, "released");
-					// IMPORTANT w
-					addListener("registered", fullname);
-					addListener("released", fullname);
-				}
-			} else {
-				log.info("{} onRegistered already registered", fullname);
-			}
-		} catch (Exception e) {
-			log.error("onRegistered threw {}", registration, e);
-		}
-	}
-
-	/**
-	 * Listener for authentication.
-	 * 
-	 * @param response The results from a foreign instance's
-	 *                 {@link Runtime#describe(String, DescribeQuery)}
-	 */
-	public void onAuthenticate(DescribeResults response) {
-		log.info("onAuthenticate {}", response);
-	}
-
-	/**
-	 * Get a list of metadata about all services local to this instance.
-	 * 
-	 * @return A list of metadata about local services
-	 * @see ServiceData#getServiceTypes()
-	 */
-	public List<MetaData> getServiceTypes() {
-		List<MetaData> filteredTypes = new ArrayList<>();
-		for (MetaData metaData : serviceData.getServiceTypes()) {
-			if (metaData.isAvailable()) {
-				filteredTypes.add(metaData);
-			}
-		}
-		return filteredTypes;
-	}
-
-	/**
-	 * Register a connection route from one instance to this one.
-	 *
-	 * @param uuid       Unique ID for a connecting client
-	 * @param id         Name or ID of the connecting client
-	 * @param connection Details of the connection
-	 */
-	@Override
-	public void addConnection(String uuid, String id, Connection connection) {
-		Connection attr = null;
-		if (!connections.containsKey(uuid)) {
-			attr = connection;
-			invoke("publishConnect", connection);
-		} else {
-			attr = connections.get(uuid);
-			attr.putAll(connection);
-		}
-		connections.put(uuid, attr);
-		// String id = (String)attr.get("id");
-
-		addRoute(id, uuid, 10);
-	}
-
-	/**
-	 * Unregister all connections that a specified client has made.
-	 *
-	 * @param uuid The ID of the client
-	 */
-	@Override
-	public void removeConnection(String uuid) {
-
-		Connection conn = connections.remove(uuid);
-
-		if (conn != null) {
-			invoke("publishDisconnect", uuid);
-			invoke("getConnections");
-
-			Set<String> remoteIds = routeTable.getAllIdsFor(uuid);
-			for (String id : remoteIds) {
-				unregisterId(id);
-			}
-			routeTable.removeRoute(uuid);
-		}
-	}
-
-	/**
-	 * Unregister all services originating from the instance with the given ID.
-	 *
-	 * @param id The ID of the instance that is being unregistered
-	 */
-	public void unregisterId(String id) {
-		Set<String> names = new HashSet<>(registry.keySet());
-		for (String name : names) {
-			if (name.endsWith("@" + id)) {
-				unregister(name);
-			}
-		}
-	}
-
-	public String publishDisconnect(String uuid) {
-		return uuid;
-	}
-
-	// FIXME - filter only serializable objects ?
-	public Connection publishConnect(Connection attributes) {
-		return attributes;
-	}
-
-	/**
-	 * globally get all client
-	 *
-	 * @return connection map
-	 */
-	public Map<String, Connection> getConnections() {
-		return connections;
-	}
-
-	/**
-	 * separated by connection - send connection name and get filter results back
-	 * for a specific connections connected clients
-	 *
-	 * @param gatwayName name
-	 * @return map of connections
-	 *
-	 */
-	public Map<String, Connection> getConnections(String gatwayName) {
-		Map<String, Connection> ret = new HashMap<>();
-		for (String uuid : connections.keySet()) {
-			Connection c = connections.get(uuid);
-			String gateway = (String) c.get("gateway");
-			if (gatwayName == null || gateway.equals(gatwayName)) {
-				ret.put(uuid, c);
-			}
-		}
-		return ret;
-	}
-
-	/**
-	 * @return list connections - current connection names to this mrl runtime
-	 *
-	 */
-	public Map<String, Connection> lc() {
-		return getConnections();
-	}
-
-	/**
-	 * get a specific clients data
-	 *
-	 * @param uuid uuid to get
-	 * @return connection for uuid
-	 *
-	 */
-	public Connection getConnection(String uuid) {
-		return connections.get(uuid);
-	}
-
-	/**
-	 * @return Globally get all connection uuids
-	 *
-	 */
-	public List<String> getConnectionUuids() {
-		return getConnectionUuids(null);
-	}
-
-	/**
-	 * Get whether a connection to the given client exists.
-	 *
-	 * @param uuid Unique ID of the client to check for
-	 * @return Whether a connection between this instance and the given client
-	 *         exists
-	 */
-	boolean connectionExists(String uuid) {
-		return connections.containsKey(uuid);
-	}
-
-	/**
-	 * Get connection ids that belong to a specific gateway
-	 *
-	 * @param name n
-	 * @return list of uuids
-	 *
-	 */
-	public List<String> getConnectionUuids(String name) {
-		List<String> ret = new ArrayList<>();
-		for (String uuid : connections.keySet()) {
-			Connection c = connections.get(uuid);
-			String gateway = (String) c.get("gateway");
-			if (name == null || gateway.equals(name)) {
-				ret.add(uuid);
-			}
-		}
-		return ret;
-	}
-
-	/**
-	 * Get the Class instance for a specific service.
-	 *
-	 * @param inName The name of the service
-	 * @return The Class of the service.
-	 * @see #getFullName(String)
-	 */
-	public static Class<?> getClass(String inName) {
-		String name = getFullName(inName);
-		ServiceInterface si = registry.get(name);
-		if (si == null) {
-			return null;
-		}
-		return si.getClass();
-	}
-
-	/**
-	 * takes an id returns a connection uuid
-	 *
-	 * @param id id
-	 * @return the connection
-	 *
-	 */
-	public Connection getRoute(String id) {
-		return connections.get(routeTable.getRoute(id));
-	}
-
-	public RouteTable getRouteTable() {
-		return routeTable;
-	}
-
-	/**
-	 * get gateway based on remote address of a msg e.g. msg.getRemoteId()
-	 *
-	 * @param remoteId remote
-	 * @return the gateway
-	 *
-	 */
-	public Gateway getGatway(String remoteId) {
-		// get a connection from the route
-		Connection conn = getRoute(remoteId);
-		if (conn == null) {
-			log.debug("no connection for id {}", remoteId);
-			return null;
-		}
-		// find the gateway managing the connection
-		return (Gateway) getService((String) conn.get("gateway"));
-	}
-
-	/**
-	 * Get the full name of the service. A full name is defined as a "short name"
-	 * plus the ID of the Runtime instance it is attached to. The two components are
-	 * separated by an '@' character. If the given name is already a full name, it
-	 * is returned immediately, otherwise a full name is constructed by assuming the
-	 * service is local to this instance. Example:
-	 * 
-	 * <pre>
-	 * {
-	 * 	&#64;code
-	 * 	String shortName = "python";
-	 *
-	 * 	// Assume the local name is "bombastic-cherry"
-	 * 	String fullName = getFullName(shortName);
-	 * 	// fullName is now "python@bombastic-cherry"
-	 *
-	 * 	fullName = getFullName(fullName);
-	 * 	// fullName is unchanged because it was already a full name
-	 *
-	 * }
-	 * </pre>
-	 *
-	 *
-	 * @param shortname The name to convert to a full name
-	 * @return shortname if it is already a full name, or a newly constructed full
-	 *         name
-	 */
-	static public String getFullName(String shortname) {
-		if (shortname == null || shortname.contains("@")) {
-			// already long form
-			return shortname;
-		}
-		// if nothing is supplied assume local
-		return String.format("%s@%s", shortname, Runtime.getInstance().getId());
-	}
-
-	@Override
-	public List<String> getClientIds() {
-		return getConnectionUuids(getName());
-	}
-
-	@Override
-	public Map<String, Connection> getClients() {
-		return getConnections(getName());
-	}
-
-	public void pollHosts() {
-		runtime.addTask(20000, "getHosts");
-	}
-
-	// FIXME - remove if not using ...
-	@Override
-	public void sendRemote(Message msg) throws IOException {
-		if (isLocal(msg)) {
-			log.error("msg NOT REMOTE yet sendRemote is called {}", msg);
-			return;
-		}
-
-		// get a connection from the route
-		Connection conn = getRoute(msg.getId());
-		if (conn == null) {
-			log.error("could not get connection for {} from msg {}", msg.getId(), msg);
-			return;
-		}
-
-		// two possible types of "remote" for this gateway cli & ws
-		if ("Cli".equals(conn.get("c-type"))) {
-			invoke("publishCli", msg);
-
-			InProcessCli cli = ((InProcessCli) conn.get("cli"));
-			cli.onMsg(msg);
-
-		} else {
-			// websocket Client !
-			WsClient client = (WsClient) conn.get("c-client");
-			if (client == null) {
-				log.error("could not get client for connection {}", msg.getId());
-				return;
-			}
-
-			/**
-			 * ======================================================================
-			 * DYNAMIC ROUTE TABLE - outbound msg hop starts now
-			 */
-
-			// add our id - we don't want to see it again
-			msg.addHop(getId());
-
-			log.info("<== {}.{} <-- {}", msg.name, msg.method, msg.sender);
-
-			/**
-			 * ======================================================================
-			 */
-
-			client.send(CodecUtils.toJsonMsg(msg));
-		}
-	}
-
-	public Object publishCli(Message msg) {
-		if (msg.data == null || msg.data.length == 0) {
-			return null;
-		}
-		return msg.data[0];
-	}
-
-	/**
-	 * DONT MODIFY NAME - JUST work on is Local - and InvokeOn should handle it
-	 *
-	 * if the incoming Message's remote Id is the (same as ours) OR (it can't be
-	 * found it our route table) - peel it off and treat it as local.
-	 *
-	 * if we have an @{id/connection} but do not have the connection - we'll peel
-	 * off the @{id/connection} and treat it as local if id is ours - peel it off !
-	 */
-	@Override
-	public boolean isLocal(Message msg) {
-
-		if (msg.getId() == null || getId().equals(msg.getId())) {
-			return true;
-		}
-
-		return false;
-	}
-
-	public Object localizeDefault(String key) {
-		key = key.toUpperCase();
-		return defaultLocalization.get(key);
-	}
-
-	static public void setAllLocales(String code) {
-		for (ServiceInterface si : getLocalServices().values()) {
-			si.setLocale(code);
-		}
-	}
-
-	@Override
-	public String created(String name) {
-		return name;
-	}
-
-	@Override
-	public String started(String name) {
-		// if this is to be used as a callback in Python
-		// users typically would want simple name ... not "fullname"
-
-		return name;
-	}
-
-	@Override
-	public String stopped(String name) {
-		return name;
-	}
-
-	/**
-	 * Wrapper for {@link ServiceData#getMetaData(String, String)}
-	 * 
-	 * @param serviceName The name of the service
-	 * @param serviceType The type of the service
-	 * @return The metadata of the service.
-	 */
-	public static MetaData getMetaData(String serviceName, String serviceType) {
-		return ServiceData.getMetaData(serviceName, serviceType);
-	}
-
-	/**
-	 * Wrapper for {@link ServiceData#getMetaData(String)}
-	 * 
-	 * @param serviceType The type of the service
-	 * @return The metadata of the service.
-	 */
-	public static MetaData getMetaData(String serviceType) {
-		return ServiceData.getMetaData(serviceType);
-	}
-
-	/**
-	 * Whether the singleton has been created
-	 * 
-	 * @return Whether the singleton exists
-	 */
-	public static boolean exists() {
-		return runtime != null;
-	}
-
-	/**
-	 * Attempt to get the most likely valid address priority would be a lan address
-	 * - possibly the smallest class
-	 *
-	 * @return string address
-	 *
-	 */
-	public String getAddress() {
-		List<String> addresses = getIpAddresses();
-		if (addresses.size() > 0) {
-
-			// class priority
-			for (String ip : addresses) {
-				if (ip.startsWith("192.168")) {
-					return ip;
-				}
-			}
-
-			for (String ip : addresses) {
-				if (ip.startsWith("172.")) {
-					return ip;
-				}
-			}
-
-			for (String ip : addresses) {
-				if (ip.startsWith("10.")) {
-					return ip;
-				}
-			}
-
-			// give up - return first :P
-			return addresses.get(0);
-		}
-		return null;
-	}
-
-	public List<Host> getHosts() {
-		List<String> ips = getIpAddresses();
-		String selectedIp = (ips.size() == 1) ? ips.get(0) : null;
-		if (selectedIp == null) {
-			for (String ip : ips) {
-				if ((selectedIp != null) && (ip.startsWith(("192.")))) {
-					selectedIp = ip;
-				} else if (selectedIp == null) {
-					selectedIp = ip;
-				}
-			}
-		}
-		String subnet = selectedIp.substring(0, selectedIp.lastIndexOf("."));
-		return getHosts(subnet);
-	}
-
-	public List<Host> getHosts(String subnet) {
-
-		if (hosts == null) {
-			hosts = new HashMap<String, Host>();
-			File check = new File(FileIO.gluePaths(getDataDir(), "hosts.json"));
-			if (check.exists()) {
-				try {
-					Host[] hf = CodecUtils.fromJson(FileIO.toString(check), Host[].class);
-					for (Host h : hf) {
-						hosts.put(h.ip, h);
-					}
-					info("found %d saved hosts", hosts.size());
-				} catch (Exception e) {
-					error("could not load %s - %s", check, e.getMessage());
-				}
-			}
-		}
-
-		int timeout = 1500;
-		try {
-			for (int i = 1; i < 255; i++) {
-				Thread pinger = new Thread(new Pinger(this, hosts, subnet + "." + i, timeout), "pinger-" + i);
-				pinger.start();
-			}
-		} catch (Exception e) {
-			log.error("getHosts threw", e);
-		}
-		List<Host> h = new ArrayList<>();
-		for (Host hst : hosts.values()) {
-			if (hst.lastActiveTs != null) {
-				h.add(hst);
-			}
-		}
-		return h;
-	}
-
-	public Host publishFoundHost(Host host) {
-		log.info("found host {}", host);
-		return host;
-	}
-
-	public Host publishFoundNewHost(Host host) {
-		log.info("found new host {}", host);
-		return host;
-	}
-
-	public Host publishLostHost(Host host) {
-		log.info("lost host {}", host);
-		return host;
-	}
-
-	public void saveHosts() throws IOException {
-		FileOutputStream fos = new FileOutputStream(FileIO.gluePaths(getDataDir(), "hosts.json"));
-		List<Host> h = new ArrayList<>(hosts.values());
-		String json = CodecUtils.toPrettyJson(h);
-		fos.write(json.getBytes());
-		fos.close();
-	}
-
-	/**
-	 * start python interactively at the command line
-	 */
-	public void python() {
-		if (cli == null) {
-			startInteractiveMode();
-		}
-		start("python", "Python");
-		// since we've suscribed to pythons st
-		cli.relay("python", "exec", "publishStdOut");
-		cli.relay("python", "exec", "publishStdError");
-		Logging logging = LoggingFactory.getInstance();
-		logging.removeAllAppenders();
-	}
-
-	/**
-	 * Main entry point for the MyRobotLab Runtime Check CmdOptions for list of
-	 * options -h help -v version -list jvm args -Dhttp.proxyHost=webproxy
-	 * f-Dhttp.proxyPort=80 -Dhttps.proxyHost=webproxy -Dhttps.proxyPort=80
-	 *
-	 * @param args cmd line args from agent spawn
-	 *
-	 */
-	public static void main(String[] args) {
-
-		try {
-
-			// loading args
-			globalArgs = args;
-			new CommandLine(options).parseArgs(args);
-			log.info("in args {}", Launcher.toString(args));
-			log.info("options {}", CodecUtils.toJson(options));
-			log.info("\n" + Launcher.banner);
-
-			// creating initial data/config directory
-			File cfgRoot = new File(ROOT_CONFIG_DIR);
-			cfgRoot.mkdirs();
-
-			// initialize logging
-			initLog();
-
-			// extract if necessary
-			FileIO.extractResources();
-
-			// help and exit
-			if (options.help) {
-				mainHelp();
-				return;
-			}
-
-			// start.yml file is required, if not pre-existing
-			// is created immediately. It contains static information
-			// which needs to be available before a Runtime is created
-			Runtime.startYml = ConfigUtils.loadStartYml();
-
-			// resolve configName before starting getting runtime configuration
-			Runtime.configName = (startYml.enable) ? startYml.config : "default";
-			if (options.config != null) {
-				// cmd line options has the highest priority
-				Runtime.configName = options.config;
-			}
-
-			// start.yml is processed, config name is set, runtime config
-			// is resolved, now we can start instance
-			Runtime.getInstance();
-
-			if (options.install != null) {
-				// resetting log level to info
-				// for an install otherwise ivy
-				// info will not be shown in the terminal
-				// during install of dependencies
-				// which makes users panic and hit ctrl+C
-				setLogLevel("info");
-
-				// we start the runtime so there is a status publisher which will
-				// display status updates from the repo install
-				log.info("requesting install");
-				Repo repo = getInstance().getRepo();
-				if (options.install.length == 0) {
-					repo.install(LIBRARIES, (String) null);
-				} else {
-					for (String service : options.install) {
-						repo.install(LIBRARIES, service);
-					}
-				}
-				shutdown();
-				return;
-			}
-
-		} catch (Exception e) {
-			log.error("runtime exception", e);
-			Runtime.mainHelp();
-			shutdown();
-			log.error("main threw", e);
-		}
-	}
-
-	public static void initLog() {
-		if (options != null) {
-			LoggingFactory.init(options.logLevel);
-		} else {
-			LoggingFactory.init("info");
-		}
-	}
-
-	public void test() {
-		for (int statusCnt = 0; statusCnt < 500; statusCnt++) {
-			statusCnt++;
-			invoke("publishStatus", Status.info("this is status %d", statusCnt));
-		}
-	}
-
-	public Connection getConnectionFromId(String remoteId) {
-		for (Connection c : connections.values()) {
-			if (c.getId().equals(remoteId)) {
-				return c;
-			}
-		}
-		return null;
-	}
-
-	/**
-	 * A gateway is responsible for creating a key to associate a unique
-	 * "Connection". This key should be retrievable, when a msg arrives at the
-	 * service which needs to be sent remotely. This key is used to get the
-	 * "Connection" to send the msg remotely
-	 *
-	 * @param string s
-	 * @param uuid   u
-	 *
-	 */
-	public void addLocalGatewayKey(String string, String uuid) {
-		routeTable.addLocalGatewayKey(string, uuid);
-	}
-
-	public boolean containsRoute(String remoteId) {
-		return routeTable.contains(remoteId);
-	}
-
-	public String getConnectionUuidFromGatewayKey(String gatewayKey) {
-		return routeTable.getConnectionUuid(gatewayKey);
-	}
-
-	/**
-	 * This helper method will create, load then start a service
-	 *
-	 * @param name - name of instance
-	 * @param type - type
-	 * @return returns the service in the form of a ServiceInterface
-	 */
-	static public ServiceInterface loadAndStart(String name, String type) {
-		ServiceInterface s = null;
-		try {
-			s = create(name, type);
-			s.load();
-			s.startService();
-		} catch (Exception e) {
-			log.error("loadAndStart threw", e);
-		}
-		return s;
-	}
-
-	/**
-	 * DEFAULT IF NOTHING EXISTS DO NOT DEFAULT SOMETHING THAT'S ALREADY IN PLAN
-	 * OVERRIDE WITH FILE
-	 * 
-	 * Load a single service entry into the plan through yml or default. This method
-	 * is responsible for resolving the Type and ServiceConfig for a single service.
-	 * Since some service Types are composites and require Peers, it can potentially
-	 * be recursive. The level of overrides are from highest priority to lowest :
-	 * 
-	 * <pre>
-	 *       if a Plan definition of {name} exists, use it   - "current" plan definition !
-	 *       /data/config/{configName}/{service}.yml          - user's yml override
-	 *       /resource/config/{configName}/{service}.yml      - system yml default
-	 *       {ServiceConfig}.java                             - system java type default
-	 * 
-	 * 
-	 * </pre>
-	 * 
-	 * @param plan  - plan to load
-	 * @param name  - name of service
-	 * @param type  - type of service
-	 * @param start - weather to specify in RuntimeConfig.registry to "start" this
-	 *              service when createFromPlan is run
-	 * @param level - level of the depth, services may load peers which in turn will
-	 *              load more, this is the depth of recursion
-	 * @return
-	 * @throws IOException
-	 */
-	public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
-		synchronized (processLock) {
-
-			if (plan == null) {
-				log.error("plan required to load a system");
-				return null;
-			}
-
-			log.info("loading - {} {} {}", name, type, level);
-			// from recursive memory definition
-			ServiceConfig sc = plan.get(name);
-
-			// HIGHEST PRIORITY - OVERRIDE WITH FILE
-			String configPath = runtime.getConfigPath();
-			String configFile = configPath + fs + name + ".yml";
-
-			// PRIORITY #1
-			// find if a current yml config file exists - highest priority
-			log.debug("priority #1 user's yml override {} ", configFile);
-			ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
-			if (fileSc != null) {
-				// if definition exists in file form, it overrides current memory one
-				sc = fileSc;
-			} else if (sc != null) {
-				// if memory config is available but not file
-				// we save it
-				String yml = CodecUtils.toYaml(sc);
-				FileIO.toFile(configFile, yml);
-			}
-
-			// special conflict case - type is specified, but its not the same as
-			// file version - in that case specified parameter type wins and
-			// overwrites
-			// config. User can force type by supplying one as a parameter, however,
-			// the
-			// recursive
-			// call other peer types will have name/file.yml definition precedence
-			if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
-				if (sc != null) {
-					warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
-				}
-				ServiceConfig.getDefault(plan, name, type);
-				sc = plan.get(name);
-
-				// create new file if it didn't exist or overwrite it if new type is
-				// required
-				String yml = CodecUtils.toYaml(sc);
-				FileIO.toFile(configFile, yml);
-			}
-
-			if (sc == null && type == null) {
-				log.error("no local config and unknown type");
-				return plan;
-			}
-
-			// finalize
-			if (sc != null) {
-				plan.put(name, sc);
-				// RECURSIVE load peers
-				Map<String, Peer> peers = sc.getPeers();
-				for (String peerKey : peers.keySet()) {
-					Peer peer = peers.get(peerKey);
-					// recursive depth load - parent and child need to be started
-					runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
-				}
-
-				// valid service config at this point - now determine if its supposed to
-				// start or not
-				// if its level 0 then it was requested by user or config - so it needs
-				// to
-				// start
-				// if its not level 0 then it was loaded because peers were defined and
-				// appropriate config loaded
-				// peer.autoStart should determine if the peer starts if not explicitly
-				// requested by the
-				// user or config
-				if (level == 0 || start) {
-					plan.addRegistry(name);
-				}
-
-			} else {
-				log.info("could not load {} {} {}", name, type, level);
-			}
-
-			return plan;
-		}
-	}
-
-	/**
-	 * read a service's configuration, in the context of current config set name or
-	 * default
-	 * 
-	 * @param name
-	 * @return
-	 */
-	public ServiceConfig readServiceConfig(String name) {
-		return readServiceConfig(name, new StaticType<>() {
-		});
-	}
-
-	/**
-	 * read a service's configuration, in the context of current config set name or
-	 * default
-	 * 
-	 * @param name
-	 * @return
-	 */
-	public <C extends ServiceConfig> C readServiceConfig(String name, StaticType<C> configType) {
-		return readServiceConfig(null, name, configType);
-	}
-
-	public ServiceConfig readServiceConfig(String configName, String name) {
-		return readServiceConfig(configName, name, new StaticType<>() {
-		});
-	}
-
-	/**
-	 *
-	 * @param configName - filename or dir of config set
-	 * @param name       - name of config file within that dir e.g. {name}.yml
-	 * @return
-	 */
-	public <C extends ServiceConfig> C readServiceConfig(String configName, String name, StaticType<C> configType) {
-		// if config path set and yaml file exists - it takes precedence
-
-		if (configName == null) {
-			configName = runtime.getConfigName();
-		}
-
-		if (configName == null) {
-			log.info("config name is null cannot load {} file system", name);
-			return null;
-		}
-
-		String filename = ROOT_CONFIG_DIR + fs + configName + fs + name + ".yml";
-		File check = new File(filename);
-		C sc = null;
-		if (check.exists()) {
-			try {
-				sc = CodecUtils.readServiceConfig(filename, configType);
-			} catch (ConstructorException e) {
-				error("config %s invalid %s %s. Please remove it from the file.", name, filename,
-						e.getCause().getMessage());
-			} catch (Exception e) {
-				error("config could not load %s file is invalid", filename);
-			}
-		}
-		return sc;
-	}
-
-	public String publishConfigLoaded(String name) {
-		return name;
-	}
-
-	@Override
-	public RuntimeConfig apply(RuntimeConfig config) {
-		super.apply(config);
-
-		setLocale(config.locale);
-
-		if (config.id == null) {
-			config.id = NameGenerator.getName();
-		}
-
-		if (config.logLevel != null) {
-			setLogLevel(config.logLevel);
-		}
-
-		if (config.virtual != null) {
-			info("setting virtual to %b", config.virtual);
-			setAllVirtual(config.virtual);
-		}
-
-		// APPLYING A RUNTIME CONFIG DOES NOT PROCESS THE REGISTRY
-		// USE startConfig(name)
-
-		broadcastState();
-		return config;
-	}
-
-	/**
-	 * release the current config
-	 */
-	static public void releaseConfig() {
-		String currentConfigPath = Runtime.getInstance().getConfigName();
-		if (currentConfigPath != null) {
-			releaseConfigPath(currentConfigPath);
-		}
-	}
-
-	/**
-	 * wrapper
-	 * 
-	 * @param configName
-	 */
-	static public void releaseConfig(String configName) {
-		setConfig(configName);
-		releaseConfigPath(Runtime.getInstance().getConfigName());
-	}
-
-	/**
-	 * Release a configuration set - this depends on a runtime file - and it will
-	 * release all the services defined in it, with the exception of the originally
-	 * started services
-	 * 
-	 * @param configPath config set to release
-	 *
-	 */
-	static public void releaseConfigPath(String configPath) {
-		try {
-			String filename = ROOT_CONFIG_DIR + fs + Runtime.getInstance().getConfigName() + fs + "runtime.yml";
-			String releaseData = FileIO.toString(new File(filename));
-			RuntimeConfig config = CodecUtils.fromYaml(releaseData, RuntimeConfig.class);
-			List<String> registry = config.getRegistry();
-			Collections.reverse(Arrays.asList(registry));
-
-			// get starting services if any entered on the command line
-			// -s log Log webgui WebGui ... etc - these will be protected
-			List<String> startingServices = new ArrayList<>();
-			if (options.services.size() % 2 == 0) {
-				for (int i = 0; i < options.services.size(); i += 2) {
-					startingServices.add(options.services.get(i));
-				}
-			}
-
-			for (String name : registry) {
-				if (startingServices.contains(name)) {
-					continue;
-				}
-				release(name);
-			}
-		} catch (Exception e) {
-			Runtime.getInstance().error("could not release %s", configPath);
-		}
-	}
-
-	public static String getConfigRoot() {
-		return ROOT_CONFIG_DIR;
-	}
-
-	/**
-	 * wrapper for saveConfigPath with default prefix path supplied
-	 * 
-	 * @param configName
-	 * @return
-	 */
-	static public boolean saveConfig(String configName) {
-		Runtime runtime = Runtime.getInstance();
-		if (configName == null) {
-			runtime.error("saveConfig require a name cannot be null");
-			return false;
-		}
-		boolean ret = runtime.saveService(configName, null, null);
-		runtime.broadcastState();
-		return ret;
-	}
-
-	/**
-	 * 
-	 * Saves the current runtime, all services and all configuration for each
-	 * service in the current "config path", if the config path does not exist will
-	 * error
-	 *
-	 * @param configName  - config set name if null defaults to default
-	 * @param serviceName - service name if null defaults to saveAll
-	 * @param filename    - if not explicitly set - will be standard yml filename
-	 * @return - true if all goes well
-	 */
-	public boolean saveService(String configName, String serviceName, String filename) {
-		try {
-
-			if (configName == null) {
-				error("config name cannot be null");
-				return false;
-			}
-
-			setConfig(configName);
-
-			String configPath = ROOT_CONFIG_DIR + fs + configName;
-
-			// save running services
-			Set<String> servicesToSave = new HashSet<>();
-
-			// conditional boolean to flip and save a config name to start.yml ?
-			if (startYml.enable) {
-				startYml.config = configName;
-				FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
-			}
-
-			if (serviceName == null) {
-				// all services
-				servicesToSave = getLocalServices().keySet();
-			} else {
-				// single service
-				servicesToSave.add(serviceName);
-			}
-
-			for (String s : servicesToSave) {
-				ServiceInterface si = getService(s);
-				// TODO - switch to save "NON FILTERED" config !!!!
-				// get filtered clone of config for saving
-				ServiceConfig config = si.getFilteredConfig();
-				String data = CodecUtils.toYaml(config);
-				String ymlFileName = configPath + fs + CodecUtils.getShortName(s) + ".yml";
-				FileIO.toFile(ymlFileName, data.getBytes());
-				info("saved %s", ymlFileName);
-			}
-
-			invoke("publishConfigList");
-			return true;
-
-		} catch (Exception e) {
-			error(e);
-		}
-		return false;
-	}
-
-	public String getConfigName() {
-		return configName;
-	}
-
-	public boolean isProcessingConfig() {
-		return processingConfig;
-	}
-
-	/**
-	 * Sets the directory for the current config. This will be under configRoot + fs
-	 * + configName. Static wrapper around setConfigName - so it can be used in the
-	 * same way as all the other common static service methods
-	 * 
-	 * @param name - config dir name under data/config/{config}
-	 * @return config dir name
-	 */
-	public static String setConfig(String name) {
-		if (name == null) {
-			log.error("config cannot be null");
-			if (runtime != null) {
-				runtime.error("config cannot be null");
-			}
-			return null;
-		}
-
-		if (name.contains(fs)) {
-			log.error("invalid character " + fs + " in configuration name");
-			if (runtime != null) {
-				runtime.error("invalid character " + fs + " in configuration name");
-			}
-			return name;
-		}
-
-		configName = name.trim();
-
-		File configDir = new File(ROOT_CONFIG_DIR + fs + name);
-		if (!configDir.exists()) {
-			configDir.mkdirs();
-		}
-
-		if (runtime != null) {
-			runtime.invoke("publishConfigList");
-			runtime.invoke("getConfigName");
-		}
-
-		return configName;
-	}
-
-	public String deleteConfig(String configName) {
-
-		File trashDir = new File(DATA_DIR + fs + "trash");
-		if (!trashDir.exists()) {
-			trashDir.mkdirs();
-		}
-
-		File configDir = new File(ROOT_CONFIG_DIR + fs + configName);
-		// Create a new directory in the trash with a timestamp to avoid name conflicts
-		File trashTargetDir = new File(trashDir, configName + "_" + System.currentTimeMillis());
-		try {
-			// Use Files.move to move the directory atomically
-			Files.move(configDir.toPath(), trashTargetDir.toPath(), StandardCopyOption.REPLACE_EXISTING);
-			log.info("Config moved to trash: " + trashTargetDir.getAbsolutePath());
-			invoke("publishConfigList");
-		} catch (IOException e) {
-			error("Failed to move config directory to trash: " + e.getMessage());
-			return null; // Return null or throw a custom exception to indicate failure
-		}
-
-		return configName;
-	}
-
-	// FIXME - move this to service and add default (no servicename) method
-	// signature
-	@Deprecated /*
-				 * I don't think this was a good solution - to handle interface lists in the js
-				 * client - the js runtime should register for lifecycle events, the individiual
-				 * services within that js runtime should only have local event handling to
-				 * change attach lists
-				 */
-	public void registerForInterfaceChange(String requestor, Class<?> interestedInterface) {
-		registerForInterfaceChange(interestedInterface.getCanonicalName());
-	}
-
-	/**
-	 * Builds the requestedAttachMatrix which is a mapping between new types and
-	 * their requested interfaces - interfaces they are interested in.
-	 *
-	 * This data should be published whenever new "Type" definitions are found
-	 *
-	 * @param targetedInterface - interface this add new interface to requested
-	 *                          interfaces - add current names of services which
-	 *                          fulfill that interface "IS ASKING"
-	 *
-	 */
-	public void registerForInterfaceChange(String targetedInterface) {
-		// boolean changed
-		Set<String> namesForRequestedInterface = interfaceToNames.get(targetedInterface);
-		if (namesForRequestedInterface == null) {
-			namesForRequestedInterface = new HashSet<>();
-			interfaceToNames.put(targetedInterface, namesForRequestedInterface);
-		}
-
-		// search through interfaceToType to find all types that implement this
-		// interface
-
-		if (interfaceToType.containsKey(targetedInterface)) {
-			Set<String> types = interfaceToType.get(targetedInterface);
-			if (types != null) {
-				for (String type : types) {
-					Set<String> names = typeToNames.get(type);
-					namesForRequestedInterface.addAll(names);
-				}
-			}
-		}
-		invoke("publishInterfaceToNames");
-	}
-
-	/**
-	 * Published whenever a new service type definition if found
-	 *
-	 * @return
-	 */
-	public Map<String, Set<String>> publishInterfaceTypeMatrix() {
-		return interfaceToType;
-	}
-
-	public Map<String, Set<String>> publishInterfaceToNames() {
-		return interfaceToNames;
-	}
-
-	static public Plan saveDefault(String className) {
-		try {
-			Runtime runtime = Runtime.getInstance();
-			return runtime.saveDefault(className.toLowerCase(), className);
-		} catch (Exception e) {
-			log.error("saving default config failed", e);
-		}
-		return null;
-	}
-
-	/**
-	 * Helper method - returns if a service is started
-	 *
-	 * @param name - name of service
-	 * @return - true if started
-	 */
-	static public boolean isStarted(String name) {
-		String fullname = null;
-		if (name == null) {
-			return false;
-		}
-		if (!name.contains("@")) {
-			fullname = name + "@" + Runtime.getInstance().getId();
-		} else {
-			fullname = name;
-		}
-		if (registry.containsKey(fullname)) {
-			ServiceInterface si = registry.get(fullname);
-			return si.isRunning();
-		}
-
-		return false;
-	}
-
-	/**
-	 * Load all configuration files from a given directory.
-	 *
-	 * @param configPath The directory to load from
-	 */
-	public static void loadConfigPath(String configPath) {
-
-		Runtime.setConfig(configPath);
-		Runtime runtime = Runtime.getInstance();
-
-		String configSetDir = runtime.getConfigName() + fs + runtime.getConfigName();
-		File check = new File(configSetDir);
-		if (configPath == null || configPath.isEmpty() || !check.exists() || !check.isDirectory()) {
-			runtime.error("config set %s does not exist or is not a directory", check.getAbsolutePath());
-			return;
-		}
-
-		File[] configFiles = check.listFiles();
-		runtime.info("%d config files found", configFiles.length);
-		for (File f : configFiles) {
-			if (!f.getName().toLowerCase().endsWith(".yml")) {
-				log.info("{} - none yml file found in config set", f.getAbsolutePath());
-			} else {
-				runtime.loadFile(f.getAbsolutePath());
-			}
-		}
-	}
-
-	/**
-	 * Load a service from a file
-	 * 
-	 * @param path The full path of the file to load - this DOES NOT set the
-	 *             configPath
-	 */
-	public void loadFile(String path) {
-		try {
-			File f = new File(path);
-			if (!f.exists() || f.isDirectory()) {
-				error("loadFile cannot load %s - it does not exist", path);
-				return;
-			}
-			String name = f.getName().substring(0, f.getName().length() - 4);
-			ServiceConfig sc = CodecUtils.readServiceConfig(path);
-			loadService(new Plan("runtime"), name, sc.type, true, 0);
-		} catch (Exception e) {
-			error("loadFile requirese");
-		}
-	}
-
-	final public Plan getDefault(String name, String type) {
-		return ServiceConfig.getDefault(new Plan("runtime"), name, type);
-	}
-
-	final public Plan saveDefault(String name, String type) {
-		return saveDefault(name, name, type, false);
-	}
-
-	final public Plan saveDefault(String name, String type, boolean fullPlan) {
-		return saveDefault(name, name, type, fullPlan);
-	}
-
-	final public Plan saveDefault(String configName, String name, String type, boolean fullPlan) {
-
-		Plan plan = ServiceConfig.getDefault(new Plan(name), name, type);
-		String configPath = ROOT_CONFIG_DIR + fs + configName;
-
-		if (!fullPlan) {
-			try {
-				String filename = configPath + fs + name + ".yml";
-				ServiceConfig sc = plan.get(name);
-				String yaml = CodecUtils.toYaml(sc);
-				FileIO.toFile(filename, yaml);
-				info("saved %s", filename);
-			} catch (IOException e) {
-				error(e);
-			}
-		} else {
-			for (String service : plan.keySet()) {
-				try {
-					String filename = configPath + fs + service + ".yml";
-					ServiceConfig sc = plan.get(service);
-					String yaml = CodecUtils.toYaml(sc);
-					FileIO.toFile(filename, yaml);
-					info("saved %s", filename);
-				} catch (IOException e) {
-					error(e);
-				}
-			}
-		}
-		return plan;
-	}
-
-	public void savePlan(String name, String type) {
-		saveDefault(name, type, true);
-	}
-
-	public void saveAllDefaults() {
-		saveAllDefaults(new File(getResourceDir()).getParent(), false);
-	}
-
-	public void saveAllDefaults(String configPath, boolean fullPlan) {
-		List<MetaData> types = serviceData.getAvailableServiceTypes();
-		for (MetaData meta : types) {
-			saveDefault(configPath + fs + meta.getSimpleName(), meta.getSimpleName().toLowerCase(),
-					meta.getSimpleName(), fullPlan);
-		}
-	}
-
-	/**
-	 * Get current runtime's config path
-	 * 
-	 * @return
-	 */
-	public String getConfigPath() {
-		return ROOT_CONFIG_DIR + fs + configName;
-	}
-
-	/**
-	 * Gets a {serviceName}.yml file config from configName directory
-	 * 
-	 * @param configName
-	 * @param serviceName
-	 * @return ServiceConfig
-	 */
-	public ServiceConfig getConfig(String configName, String serviceName) {
-		return readServiceConfig(configName, serviceName);
-	}
-
-	/**
-	 * Get a {serviceName}.yml file in the current config directory
-	 * 
-	 * @param serviceName
-	 * @return
-	 */
-	public ServiceConfig getConfig(String serviceName) {
-		return readServiceConfig(serviceName);
-	}
-
-	/**
-	 * Save a config with a new Config
-	 * 
-	 * @param name
-	 * @param serviceConfig
-	 * @throws IOException
-	 */
-	public static void saveConfig(String name, ServiceConfig serviceConfig) throws IOException {
-		String file = Runtime.ROOT_CONFIG_DIR + fs + runtime.getConfigName() + fs + name + ".yml";
-		FileIO.toFile(file, CodecUtils.toYaml(serviceConfig));
-	}
-
-	/**
-	 * get the service's peer config
-	 * 
-	 * @param serviceName
-	 * @param peerKey
-	 * @return
-	 */
-	public ServiceConfig getPeerConfig(String serviceName, String peerKey) {
-		ServiceConfig sc = runtime.getConfig(serviceName);
-		if (sc == null) {
-			return null;
-		}
-		Peer peer = sc.getPeer(peerKey);
-		return runtime.getConfig(peer.name);
-	}
-
-	/**
-	 * Switches a service's .yml type definition while replacing the set of
-	 * listeners to preserver subscriptions. Useful when switching services that
-	 * support the same interface like SpeechSynthesis services etc.
-	 * 
-	 * @param serviceName
-	 * @param type
-	 * @return
-	 */
-	public boolean changeType(String serviceName, String type) {
-		try {
-			ServiceConfig sc = getConfig(serviceName);
-			if (sc == null) {
-				error("could not find %s config", serviceName);
-				return false;
-			}
-			// get target
-			Plan targetPlan = getDefault(serviceName, type);
-			if (targetPlan == null || targetPlan.get(serviceName) == null) {
-				error("%s null", type);
-				return false;
-			}
-			ServiceConfig target = targetPlan.get(serviceName);
-			// replacing listeners
-			target.listeners = sc.listeners;
-			saveConfig(serviceName, target);
-			return true;
-		} catch (Exception e) {
-			error("could not save %s of type %s", serviceName, type);
-			return false;
-		}
-	}
-
-	/**
-	 * Get a peer's config
-	 * 
-	 * @param sericeName
-	 * @param peerKey
-	 * @return
-	 */
-	public ServiceConfig getPeer(String sericeName, String peerKey) {
-		ServiceConfig sc = getConfig(sericeName);
-		if (sc == null) {
-			return null;
-		}
-		Peer peer = sc.getPeer(peerKey);
-		if (peer == null) {
-			return null;
-		}
-		return getConfig(peer.name);
-	}
-
-	/**
-	 * Removes a config set and all its files
-	 * 
-	 * @param configName - name of config
-	 */
-	public static void removeConfig(String configName) {
-		try {
-			log.info("removing config");
-
-			File check = new File(ROOT_CONFIG_DIR + fs + configName);
-
-			if (check.exists()) {
-				Path pathToBeDeleted = Paths.get(check.getAbsolutePath());
-				Files.walk(pathToBeDeleted).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
-			}
-		} catch (Exception e) {
-			log.error("removeConfig threw", e);
-		}
-	}
+public class Runtime extends Service<RuntimeConfig> implements MessageListener, ServiceLifeCyclePublisher, RemoteMessageHandler, ConnectionManager, Gateway, LocaleProvider {
+
+  final static private long serialVersionUID = 1L;
+
+  // FIXME - AVOID STATIC FIELDS !!! use .getInstance() to get the singleton
+
+  /**
+   * a registry of all services regardless of which environment they came from -
+   * each must have a unique name
+   */
+  static volatile private Map<String, ServiceInterface> registry = new TreeMap<>();
+
+  /**
+   * A plan is a request to runtime to change the system. Typically its to ask
+   * to start and configure new services. The master plan is an accumulation of
+   * all these requests.
+   */
+  @Deprecated /* use the filesystem only no memory plan */
+  protected final Plan masterPlan = new Plan("runtime");
+
+  /**
+   * thread for non-blocking install of services
+   */
+  static private transient Thread installerThread = null;
+
+  /**
+   * services which want to know if another service with an interface they are
+   * interested in registers or is released
+   *
+   * requestor type &gt; interface &gt; set of applicable service names
+   */
+  protected final Map<String, Set<String>> interfaceToNames = new HashMap<>();
+
+  protected final Map<String, Set<String>> typeToNames = new HashMap<>();
+
+  protected final Map<String, Set<String>> interfaceToType = new HashMap<>();
+
+  protected final Map<String, Set<String>> typeToInterface = new HashMap<>();
+
+  private transient static final Object processLock = new Object();
+
+  /**
+   * FILTERED_INTERFACES are the set of low level interfaces which we are
+   * interested in filtering out if we want to maintain a data structure which
+   * has "interfaces of interest"
+   */
+  protected final static Set<String> FILTERED_INTERFACES = new HashSet<>(Arrays.asList("org.myrobotlab.framework.interfaces.Broadcaster",
+      "org.myrobotlab.service.interfaces.QueueReporter", "org.myrobotlab.framework.interfaces.ServiceQueue", "org.myrobotlab.framework.interfaces.MessageSubscriber",
+      "org.myrobotlab.framework.interfaces.Invoker", "java.lang.Runnable", "org.myrobotlab.framework.interfaces.ServiceStatus", "org.atmosphere.nettosphere.Handler",
+      "org.myrobotlab.framework.interfaces.NameProvider", "org.myrobotlab.framework.interfaces.NameTypeProvider", "org.myrobotlab.framework.interfaces.ServiceInterface",
+      "org.myrobotlab.framework.interfaces.TaskManager", "org.myrobotlab.framework.interfaces.LoggingSink", "org.myrobotlab.framework.interfaces.StatusPublisher",
+      "org.myrobotlab.framework.interfaces.TypeProvider", "java.io.Serializable", "org.myrobotlab.framework.interfaces.Attachable",
+      "org.myrobotlab.framework.interfaces.StateSaver", "org.myrobotlab.framework.interfaces.MessageSender", "java.lang.Comparable",
+      "org.myrobotlab.service.interfaces.ServiceLifeCycleListener", "org.myrobotlab.framework.interfaces.StatePublisher"));
+
+  protected final Set<String> serviceTypes = new HashSet<>();
+
+  /**
+   * The directory name currently being used for config. This is NOT full path
+   * name. It cannot be null, it cannot have "/" or "\" in the name - it has to
+   * be a valid file name for the OS. It's defaulted to "default". Changed often
+   */
+  protected static String configName = "default";
+
+  /**
+   * The runtime config which Runtime was started with. This is the config which
+   * will be applied to Runtime when its created on startup.
+   */
+  // protected static RuntimeConfig startConfig = null;
+
+  /**
+   * State variable reporting if runtime is currently starting services from
+   * config. If true you can find which config from runtime.getConfigName()
+   */
+  boolean processingConfig = false;
+
+  /**
+   * <pre>
+   * The set of client connections to this mrl instance Some of the connections
+   * are outbound to other webguis, others may be inbound if a webgui is
+   * listening in this instance. These details and many others (such as from
+   * which connection a client is from) is in the Map &lt;String, Object&gt; information.
+   * Since different connections have different requirements, and details regarding
+   * clients the only "fixed" required info to add a client is :
+   *
+   * uuid - key unique identifier for the client
+   * connection - name of the connection currently managing the clients connection
+   * state - state of the client and/or connection
+   * (lots more attributes with the Map&lt;String, Object&gt; to provide necessary data for the connection)
+   * </pre>
+   */
+  protected final Map<String, Connection> connections = new HashMap<>();
+
+  /**
+   * corrected route table with (soon to be regex ids) mapped to
+   * gateway/interfaces
+   */
+  protected final RouteTable routeTable = new RouteTable();
+
+  static private final String RUNTIME_NAME = "runtime";
+
+  /**
+   * user's data directory
+   */
+  static public final String DATA_DIR = "data";
+
+  /**
+   * default parent path of configPath static !
+   */
+  public final static String ROOT_CONFIG_DIR = DATA_DIR + fs + "config";
+
+  /**
+   * number of services created by this runtime
+   */
+  protected Integer creationCount = 0;
+
+  /**
+   * the local repo.json manifest of this machine, which is a list of all
+   * libraries ivy installed
+   */
+  transient private IvyWrapper repo = null; // was transient abstract Repo
+
+  transient private ServiceData serviceData = ServiceData.getLocalInstance();
+
+  /**
+   * command line options
+   */
+  static CmdOptions options = new CmdOptions();
+
+  /**
+   * command line configuration
+   */
+  static StartYml startYml = new StartYml();
+
+  /**
+   * the platform (local instance) for this runtime. It must be a non-static as
+   * multiple runtimes will have different platforms
+   */
+  protected Platform platform = null;
+
+  private static long uniqueID = new Random(System.currentTimeMillis()).nextLong();
+
+  public final static Logger log = LoggerFactory.getLogger(Runtime.class);
+
+  /**
+   * Object used to synchronize initializing this singleton.
+   */
+  transient private static final Object INSTANCE_LOCK = new Object();
+
+  /**
+   * The singleton of this class.
+   */
+  transient private static Runtime runtime = null;
+
+  private List<String> jvmArgs;
+
+  /**
+   * set of known hosts
+   */
+  private transient Map<String, Host> hosts = null;
+
+  /**
+   * global startingArgs - whatever came into main each runtime will have its
+   * individual copy
+   */
+  // FIXME - remove static !!!
+  static String[] globalArgs;
+
+  static Set<String> networkPeers = null;
+
+  /**
+   * The name of the folder used to store native library dependencies during
+   * installation and runtime.
+   */
+  private static final String LIBRARIES = "libraries";
+
+  String stdCliUuid = null;
+
+  InProcessCli cli = null;
+
+  /**
+   * available Locales
+   */
+  protected Map<String, Locale> locales;
+
+  protected List<String> configList;
+
+  /**
+   * Wraps {@link java.lang.Runtime#availableProcessors()}.
+   *
+   * @return the number of processors available to the Java virtual machine.
+   * @see java.lang.Runtime#availableProcessors()
+   *
+   */
+  public static final int availableProcessors() {
+    return java.lang.Runtime.getRuntime().availableProcessors();
+  }
+
+  /**
+   * Function to test if internet connectivity is available. If it is, will
+   * return the public gateway address of this computer by sending a request to
+   * an external server. If there is no internet, returns null.
+   * 
+   * @return The public IP address or null if no internet available
+   */
+  static public String getPublicGateway() {
+    try {
+
+      URL url = new URL("http://checkip.amazonaws.com/");
+      HttpURLConnection con = (HttpURLConnection) url.openConnection();
+      con.setRequestMethod("GET");
+
+      int status = con.getResponseCode();
+      log.info("status " + status);
+
+      String gateway = FileIO.toString(con.getInputStream());
+      return gateway;
+
+    } catch (Exception e) {
+      log.warn("internet not available");
+    }
+    return null;
+  }
+
+  /**
+   * Create which only has name (no type). This is only possible, if there is an
+   * appropriately named service config in the Plan (in memory) or (more
+   * commonly) on the filesystem. Since ServiceConfig comes with type
+   * information, a name is all that is needed to start the service.
+   * 
+   * @param name
+   * @return
+   */
+  static public ServiceInterface create(String name) {
+    return create(name, null);
+  }
+
+  /**
+   * Create create(name, type) goes through the full service lifecycle of:
+   *
+   * <pre>
+   * clear - clearing the plan for construction of service(s) needed 
+   * load  - loading the plan for desired services 
+   * check - checking all planned service have met appropriate licensing and dependency checks create -
+   * </pre>
+   *
+   * @param name
+   *          - Required, cannot be null
+   * @param type
+   *          - Can be null if a service file exists for named service
+   * @return the service
+   */
+  static public ServiceInterface create(String name, String type) {
+
+    synchronized (processLock) {
+
+      try {
+        ServiceInterface si = Runtime.getService(name);
+        if (si != null) {
+          return si;
+        }
+
+        Plan plan = Runtime.load(name, type);
+        Runtime.check(name, type);
+        // at this point - the plan should be loaded, now its time to create the
+        // children peers
+        // and parent service
+        createServicesFromPlan(plan, null, name);
+        si = Runtime.getService(name);
+        if (si == null) {
+          Runtime.getInstance().error("coult not create %s of type %s", name, type);
+        }
+        return si;
+      } catch (Exception e) {
+        runtime.error(e);
+      }
+      return null;
+    }
+  }
+
+  /**
+   * Creates all services necessary for this service - "all peers" and the
+   * parent service too. At this point all type information and configuration
+   * should be defined in the plan.
+   * 
+   * FIXME - should Plan be passed in as param ?
+   *
+   * @param name
+   * @return
+   */
+  private static Map<String, ServiceInterface> createServicesFromPlan(Plan plan, Map<String, ServiceInterface> createdServices, String name) {
+
+    synchronized (processLock) {
+
+      if (createdServices == null) {
+        createdServices = new LinkedHashMap<>();
+      }
+
+      // Plan's config
+      RuntimeConfig plansRtConfig = (RuntimeConfig) plan.get("runtime");
+      // current Runtime config
+      RuntimeConfig currentConfig = Runtime.getInstance().config;
+
+      for (String service : plansRtConfig.getRegistry()) {
+        ServiceConfig sc = plan.get(service);
+        if (sc == null) {
+          runtime.error("could not get %s from plan", service);
+          continue;
+        }
+        ServiceInterface si = createService(service, sc.type, null);
+        // process the base listeners/subscription of ServiceConfig
+        si.addConfigListeners(sc);
+        if (si instanceof ConfigurableService) {
+          try {
+            ((ConfigurableService) si).apply(sc);
+          } catch (Exception e) {
+            Runtime.getInstance().error("could not apply config of type %s to service %s, using default config", sc.type, si.getName(), sc.type);
+          }
+        }
+        createdServices.put(service, si);
+        currentConfig.add(service);
+      }
+
+      return createdServices;
+    }
+  }
+
+  public String getServiceExample(String serviceType) {
+    String url = "https://raw.githubusercontent.com/MyRobotLab/myrobotlab/develop/src/main/resources/resource/" + serviceType + "/" + serviceType + ".py";
+    byte[] bytes = Http.get(url);
+    if (bytes != null) {
+      return new String(bytes);
+    }
+    return "";
+  }
+
+  public static String getPeerName(String peerKey, ServiceConfig config, Map<String, ServiceReservation> peers, String parentName) {
+
+    if (peerKey == null || !peers.containsKey(peerKey)) {
+      return null;
+    }
+
+    if (config != null) {
+
+      // dynamically get config peer name
+      // e.g. tilt should be a String value in config.tilt
+      Field[] fs = config.getClass().getDeclaredFields();
+      for (Field f : fs) {
+        if (peerKey.equals(f.getName())) {
+          if (f.canAccess(config)) {
+            Object o;
+            try {
+              o = f.get(config);
+
+              if (o == null) {
+                // config "has" the field, just set to null at the moment
+                // peer actual name then will be default notation
+                if (parentName != null) {
+                  return String.format("%s.%s", parentName, peerKey);
+                }
+                log.warn("config has field named {} but it's null", peerKey);
+                return null;
+              }
+
+              if (o instanceof String) {
+                return (String) o;
+              } else {
+                log.error("config has field named {} but it is not a string", peerKey);
+                break;
+              }
+            } catch (Exception e) {
+              log.error("getting access to field threw", e);
+            }
+
+          } else {
+            log.error("config with field name {} but cannot access it", peerKey);
+          }
+        }
+      }
+    }
+    // last ditch attempt at getting the name - will default it if parentName is
+    // supplied
+    if (parentName != null) {
+      return String.format("%s.%s", parentName, peerKey);
+    }
+    return null;
+  }
+
+  public static void check(String name, String type) {
+    log.info("check - implement - dependencies and licensing");
+    // iterate through plan - check dependencies and licensing
+  }
+
+  /**
+   * Use {@link #start(String, String)} instead.
+   *
+   * @param name
+   *          Name of service
+   * @param type
+   *          Type of service
+   * @return Created service
+   */
+  @Deprecated /* use start */
+  static public ServiceInterface createAndStart(String name, String type) {
+    return start(name, type);
+  }
+
+  /**
+   * creates and starts services from a cmd line object
+   *
+   * @param services
+   *          - services to be created
+   */
+  public final static void createAndStartServices(List<String> services) {
+
+    if (services == null) {
+      log.error("createAndStartServices(null)");
+      return;
+    }
+
+    log.info("services {}", Arrays.toString(services.toArray()));
+
+    if (services.size() % 2 == 0) {
+
+      for (int i = 0; i < services.size(); i += 2) {
+        String name = services.get(i);
+        String type = services.get(i + 1);
+
+        log.info("attempting to invoke : {} of type {}", name, type);
+
+        ServiceInterface s = Runtime.create(name, type);
+
+        if (s != null) {
+          try {
+            s.startService();
+          } catch (Exception e) {
+            runtime.error(e.getMessage());
+            Logging.logError(e);
+          }
+        } else {
+          runtime.error(String.format("could not create service %s %s", name, type));
+        }
+
+      }
+      return;
+    }
+    Runtime.mainHelp();
+    shutdown();
+  }
+
+  /**
+   * Setting the runtime virtual will set the platform virtual too. All
+   * subsequent services will be virtual
+   */
+  @Override
+  public boolean setVirtual(boolean b) {
+    boolean changed = config.virtual != b;
+    config.virtual = b;
+    isVirtual = b;
+    setAllVirtual(b);
+    if (changed) {
+      broadcastState();
+    }
+    return b;
+  }
+
+  /**
+   * Sets all services' virtual state to {@code b}. This allows a single call to
+   * enable or disable virtualization across all services.
+   *
+   * @param b
+   *          Whether all services should be virtual or not
+   * @return b
+   */
+  static public boolean setAllVirtual(boolean b) {
+    for (ServiceInterface si : getServices()) {
+      if (!si.isRuntime()) {
+        si.setVirtual(b);
+      }
+    }
+    Runtime.getInstance().config.virtual = b;
+    Runtime.getInstance().broadcastState();
+    return b;
+  }
+
+  /**
+   * Sets the enable value in start.yml. start.yml is a file which can control
+   * the automatic loading of config. In general when its on, and a config is
+   * selected and saved, the next time Runtime starts it will attempt to load
+   * the last saved config and get the user back to their last state.
+   * 
+   * @param autoStart
+   * @throws IOException
+   *           - thrown if cannot write file to filesystem
+   */
+  public void setAutoStart(boolean autoStart) throws IOException {
+    log.debug("setAutoStart {}", autoStart);
+    startYml.enable = autoStart;
+    startYml.config = configName;
+    FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
+    invoke("getStartYml");
+  }
+
+  /**
+   * Framework owned method - core of creating a new service. This method will
+   * create a service with the given name and of the given type. If the type
+   * does not contain any dots, it will be assumed to be in the
+   * {@code org.myrobotlab.service} package. This method can currently only
+   * instantiate Java services, but in the future it could be enhanced to call
+   * native service runtimes.
+   * <p>
+   * The name parameter must not contain '/' or '@'. Thus, a full name must be
+   * split into its first and second part, passing the first in as the name and
+   * the second as the inId. This method will log an error and return null if
+   * name contains either of those two characters.
+   * <p>
+   * The {@code inId} is used to determine whether the service is a local one or
+   * a remote proxy. It should equal the Runtime ID of the MyRobotLab instance
+   * the service was originally instantiated under.
+   * 
+   * @param name
+   *          May not contain '/' or '@', i.e. cannot be a full name
+   * @param type
+   *          The type of the new service
+   * @param inId
+   *          The ID of the runtime the service is linked to.
+   * @return An existing service if the requested name and type match, otherwise
+   *         a newly created service. If the name is null, or it contains '@' or
+   *         '/', or a service with the same name exists but has a different
+   *         type, will return null instead.
+   */
+  static private ServiceInterface createService(String name, String type, String inId) {
+    synchronized (processLock) {
+      log.info("Runtime.createService {}", name);
+
+      if (name == null) {
+        runtime.error("service name cannot be null");
+
+        return null;
+      }
+
+      if (name.contains("@") || name.contains("/")) {
+        runtime.error("service name cannot contain '@' or '/': {}", name);
+
+        return null;
+      }
+
+      String fullName;
+      if (inId == null || inId.equals(""))
+        fullName = getFullName(name);
+      else
+        fullName = String.format("%s@%s", name, inId);
+
+      if (type == null) {
+        ServiceConfig sc;
+        try {
+          sc = CodecUtils.readServiceConfig(runtime.getConfigName() + fs + name + ".yml");
+        } catch (IOException e) {
+          runtime.error("could not find type for service %s", name);
+          return null;
+        }
+        if (sc != null) {
+          log.info("found type for {} in plan", name);
+          type = sc.type;
+        } else {
+          runtime.error("createService type not specified and could not get type for {} from plan", name);
+          return null;
+        }
+      }
+
+      if (type == null) {
+        runtime.error("cannot create service {} no type in plan or yml file", name);
+        return null;
+      }
+
+      String fullTypeName = CodecUtils.makeFullTypeName(type);
+
+      ServiceInterface si = Runtime.getService(fullName);
+      if (si != null) {
+        if (!si.getTypeKey().equals(fullTypeName)) {
+          runtime.error("Service with name {} already exists but is of type {} while requested type is ", name, si.getTypeKey(), type);
+          return null;
+        }
+        return si;
+      }
+
+      // DO NOT LOAD HERE !!! - doing so would violate the service life cycle !
+      // only try to resolve type by the plan - if not then error out
+
+      String id = (inId == null) ? Runtime.getInstance().getId() : inId;
+      if (name.length() == 0 || fullTypeName == null || fullTypeName.length() == 0) {
+        log.error("{} not a type or {} not defined ", fullTypeName, name);
+        return null;
+      }
+
+      // TODO - test new create of existing service
+      ServiceInterface sw = Runtime.getService(String.format("%s@%s", name, id));
+      if (sw != null) {
+        log.info("service {} already exists", name);
+        return sw;
+      }
+
+      try {
+
+        if (log.isDebugEnabled()) {
+          // TODO - determine if there have been new classes added from
+          // ivy --> Boot Classloader --> Ext ClassLoader --> System
+          // ClassLoader
+          // http://blog.jamesdbloom.com/JVMInternals.html
+          log.debug("ABOUT TO LOAD CLASS");
+          log.debug("loader for this class " + Runtime.class.getClassLoader().getClass().getCanonicalName());
+          log.debug("parent " + Runtime.class.getClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("system class loader " + ClassLoader.getSystemClassLoader());
+          log.debug("parent should be null" + ClassLoader.getSystemClassLoader().getParent().getClass().getCanonicalName());
+          log.debug("thread context " + Thread.currentThread().getContextClassLoader().getClass().getCanonicalName());
+          log.debug("thread context parent " + Thread.currentThread().getContextClassLoader().getParent().getClass().getCanonicalName());
+        }
+
+        // FIXME - error if deps are missing - prompt license
+        // require restart !
+        // FIXME - this should happen after inspecting the "loaded" "plan" not
+        // during the create/start/apply !
+
+        // create an instance
+        Object newService = Instantiator.getThrowableNewInstance(null, fullTypeName, name, id);
+        log.debug("returning {}", fullTypeName);
+        si = (ServiceInterface) newService;
+
+        // si.setId(id);
+        if (Runtime.getInstance().getId().equals(id)) {
+          si.setVirtual(Runtime.getInstance().isVirtual());
+          Runtime.getInstance().creationCount++;
+          si.setOrder(Runtime.getInstance().creationCount);
+        }
+
+        if (runtime != null) {
+
+          runtime.invoke("created", getFullName(name));
+
+          // add all the service life cycle subscriptions
+          // runtime.addListener("registered", name);
+          // runtime.addListener("created", name);
+          // runtime.addListener("started", name);
+          // runtime.addListener("stopped", name);
+          // runtime.addListener("released", name);
+        }
+
+        return (Service) newService;
+      } catch (Exception e) {
+        log.error("createService failed for {}@{} of type {}", name, id, fullTypeName, e);
+      }
+      return null;
+    }
+  }
+
+  static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries() {
+    return getNotifyEntries(null);
+  }
+
+  static public Map<String, Map<String, List<MRLListener>>> getNotifyEntries(String service) {
+    Map<String, Map<String, List<MRLListener>>> ret = new TreeMap<String, Map<String, List<MRLListener>>>();
+    Map<String, ServiceInterface> sorted = null;
+    if (service == null) {
+      sorted = getLocalServices();
+    } else {
+      sorted = new HashMap<String, ServiceInterface>();
+      ServiceInterface si = Runtime.getService(service);
+      if (si != null) {
+        sorted.put(service, si);
+      }
+    }
+    for (Map.Entry<String, ServiceInterface> entry : sorted.entrySet()) {
+      log.info(entry.getKey() + "/" + entry.getValue());
+      List<String> flks = entry.getValue().getNotifyListKeySet();
+      Map<String, List<MRLListener>> subret = new TreeMap<String, List<MRLListener>>();
+      for (String sn : flks) {
+        List<MRLListener> mrllistners = entry.getValue().getNotifyList(sn);
+        subret.put(sn, mrllistners);
+      }
+      ret.put(entry.getKey(), subret);
+    }
+    return ret;
+  }
+
+  /**
+   * Dumps {@link #registry} to a file called {@code registry.json} in JSON
+   * form.
+   *
+   * @return The registry in JSON form or null if an error occurred.
+   */
+  public static String dump() {
+    try {
+      FileOutputStream dump = new FileOutputStream("registry.json");
+      String reg = CodecUtils.toJson(registry);
+      dump.write(reg.getBytes());
+      dump.close();
+      return reg;
+    } catch (Exception e) {
+      log.error("dump threw", e);
+    }
+    return null;
+  }
+
+  /**
+   * Wraps {@link java.lang.Runtime#gc()}.
+   *
+   * Runs the garbage collector.
+   */
+  public static final void gc() {
+    java.lang.Runtime.getRuntime().gc();
+  }
+
+  /**
+   * Although "fragile" since it relies on a external source - its useful to
+   * find the external ip address of NAT'd systems
+   *
+   * @return external or routers ip
+   * @throws Exception
+   *           e
+   */
+  public static String getExternalIp() throws Exception {
+    URL whatismyip = new URL("http://checkip.amazonaws.com");
+    BufferedReader in = null;
+    try {
+      in = new BufferedReader(new InputStreamReader(whatismyip.openStream()));
+      String ip = in.readLine();
+      return ip;
+    } finally {
+      if (in != null) {
+        try {
+          in.close();
+        } catch (IOException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+  }
+
+  /**
+   * Wraps {@link java.lang.Runtime#freeMemory()}.
+   *
+   * @return the amount of free memory in the Java Virtual Machine. Calling the
+   *         gc method may result in increasing the value returned by
+   *         freeMemory.
+   *
+   *
+   */
+  public static final long getFreeMemory() {
+    return java.lang.Runtime.getRuntime().freeMemory();
+  }
+
+  /**
+   * Get a handle to the Runtime singleton.
+   *
+   * @return the Runtime
+   */
+  public static Runtime getInstance() {
+    if (runtime == null) {
+      synchronized (INSTANCE_LOCK) {
+        try {
+
+          RuntimeConfig c = null;
+          if (runtime == null) {
+            c = ConfigUtils.loadRuntimeConfig(options);
+            runtime = (Runtime) createService(RUNTIME_NAME, "Runtime", c.id);
+            runtime.startService();
+            // klunky
+            Runtime.register(new Registration(runtime));
+          }
+
+          runtime.locales = Locale.getDefaults();
+
+          runtime.getRepo().addStatusPublisher(runtime);
+          runtime.startService();
+          // extract resources "if a jar"
+          FileIO.extractResources();
+          runtime.startInteractiveMode();
+          if (c != null) {
+            runtime.apply(c);
+          }
+
+          if (options.services != null && options.services.size() != 0) {
+            log.info("command line services were specified");
+            createAndStartServices(options.services);
+          }
+
+          if (options.config != null) {
+            log.info("command line -c config was specified");
+            Runtime.startConfig(options.config);
+          }
+
+          if (startYml.enable && startYml.config != null) {
+            log.info("start.yml is enabled and config is {}", startYml.config);
+            Runtime.startConfig(startYml.config);
+          }
+
+        } catch (Exception e) {
+          log.error("runtime getInstance threw", e);
+        }
+      } // synchronized lock
+    }
+
+    return runtime;
+  }
+
+  /**
+   * The jvm args which started this process
+   *
+   * @return all jvm args in a list
+   */
+  static public List<String> getJvmArgs() {
+    RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
+    return runtimeMxBean.getInputArguments();
+  }
+
+  /**
+   * gets all non-loopback, active, non-virtual ip addresses
+   *
+   * @return list of local ipv4 IP addresses
+   */
+  static public List<String> getIpAddresses() {
+    log.debug("getLocalAddresses");
+    ArrayList<String> ret = new ArrayList<String>();
+
+    try {
+      Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+      while (interfaces.hasMoreElements()) {
+        NetworkInterface current = interfaces.nextElement();
+        // log.info(current);
+        if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
+          log.debug("skipping interface is down, a loopback or virtual");
+          continue;
+        }
+        Enumeration<InetAddress> addresses = current.getInetAddresses();
+        while (addresses.hasMoreElements()) {
+          InetAddress currentAddress = addresses.nextElement();
+
+          if (!(currentAddress instanceof Inet4Address)) {
+            log.debug("not ipv4 skipping");
+            continue;
+          }
+
+          if (currentAddress.isLoopbackAddress()) {
+            log.debug("skipping loopback address");
+            continue;
+          }
+          log.debug(currentAddress.getHostAddress());
+          ret.add(currentAddress.getHostAddress());
+        }
+      }
+    } catch (Exception e) {
+      Logging.logError(e);
+    }
+
+    if (ret.size() == 0) {
+      // if we don't have a "real" ip address - we always have home
+      ret.add("127.0.0.1");
+    }
+    return ret;
+  }
+
+  // What's the purpose of this? It doesn't return anything
+  static public void getNetInfo() {
+    try {
+      List<String> local = getIpAddresses();
+      String gateway = getPublicGateway();
+      getNetworkPeers();
+    } catch (Exception e) {
+      log.error("getNetInfo threw", e);
+    }
+
+  }
+
+  // TODO - add network to search
+  static public Set<String> getNetworkPeers() throws UnknownHostException {
+    networkPeers = new TreeSet<>();
+    // String myip = InetAddress.getLocalHost().getHostAddress();
+    List<String> myips = getIpAddresses(); // TODO - if nothing else -
+    // 127.0.0.1
+    for (String myip : myips) {
+      if (myip.equals("127.0.0.1")) {
+        log.info("This PC is not connected to any network!");
+      } else {
+        String testIp = null;
+        for (int i = myip.length() - 1; i >= 0; --i) {
+          if (myip.charAt(i) == '.') {
+            testIp = myip.substring(0, i + 1);
+            break;
+          }
+        }
+
+        log.info("My Device IP: " + myip + "\n");
+        log.info("Search log:");
+
+        for (int i = 1; i <= 254; ++i) {
+          try {
+
+            InetAddress addr = InetAddress.getByName(testIp + new Integer(i).toString());
+
+            if (addr.isReachable(1000)) {
+              log.info("Available: " + addr.getHostAddress());
+              networkPeers.add(addr.getHostAddress());
+            } else {
+              log.info("Not available: " + addr.getHostAddress());
+            }
+
+            // TODO - check default port 8888 8887
+
+          } catch (IOException ioex) {
+          }
+        }
+
+        log.info("found {} devices", networkPeers.size());
+
+        for (String device : networkPeers) {
+          log.info(device);
+        }
+      }
+    }
+    return networkPeers;
+  }
+
+  static public List<ApiDescription> getApis() {
+    return CodecUtils.getApis();
+  }
+
+  // @TargetApi(9)
+  static public List<String> getLocalHardwareAddresses() {
+    log.info("getLocalHardwareAddresses");
+    ArrayList<String> ret = new ArrayList<String>();
+    try {
+      Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+      while (interfaces.hasMoreElements()) {
+        NetworkInterface current = interfaces.nextElement();
+        byte[] mac = current.getHardwareAddress();
+
+        if (mac == null || mac.length == 0) {
+          continue;
+        }
+
+        String m = StringUtil.bytesToHex(mac);
+        log.info("mac address : {}", m);
+        ret.add(m);
+        log.info("added mac");
+      }
+    } catch (Exception e) {
+      log.error("getLocalHardwareAddresses threw", e);
+    }
+
+    log.info("done");
+    return ret;
+  }
+
+  /**
+   * Gets a Map between service names and the service object of all services
+   * local to this MRL instance.
+   * 
+   * @return A Map between service names and service objects
+   */
+  public static Map<String, ServiceInterface> getLocalServices() {
+    Map<String, ServiceInterface> local = new HashMap<>();
+    for (String serviceName : registry.keySet()) {
+      // FIXME @ should be a requirement of "all" entries for consistency
+      if (!serviceName.contains("@") || serviceName.endsWith(String.format("@%s", Runtime.getInstance().getId()))) {
+        local.put(serviceName, registry.get(serviceName));
+      }
+    }
+    return local;
+  }
+
+  /**
+   * FIXME - return
+   *
+   * @return filtering/query requests
+   */
+  public static Map<String, ServiceInterface> getLocalServicesForExport() {
+    return registry;
+  }
+
+  /*
+   * FIXME - DEPRECATE - THIS IS NOT "instance" specific info - its Class
+   * definition info - Runtime should return based on ClassName
+   *
+   * FIXME - INPUT PARAMETER SHOULD BE TYPE NOT INSTANCE NAME !!!!
+   */
+  public static Map<String, MethodEntry> getMethodMap(String inName) {
+    String serviceName = getFullName(inName);
+    if (!registry.containsKey(serviceName)) {
+      runtime.error(String.format("%1$s not in registry - can not return method map", serviceName));
+      return null;
+    }
+
+    ServiceInterface sw = registry.get(serviceName);
+    Class<?> c = sw.getClass();
+
+    MethodCache cache = MethodCache.getInstance();
+    return cache.getRemoteMethods(c.getTypeName());
+
+  }
+
+  /**
+   * getServiceList returns the most important identifiers for a service which
+   * are it's process id, it's name, and it's type.
+   * <p>
+   * This will be part of the getHelloRequest - and the first listing from a
+   * process of what services are available.
+   * <p>
+   * TODO - future work would be to supply a query to the getServiceList(query)
+   * such that interfaces, types, or processes ids, can selectively be queried
+   * out of it
+   *
+   * @return list of registrations
+   */
+  public List<Registration> getServiceList() {
+    synchronized (processLock) {
+      return registry.values().stream().map(si -> new Registration(si.getId(), si.getName(), si.getTypeKey())).collect(Collectors.toList());
+    }
+  }
+
+  // FIXME - scary function - returns private data
+  public static Map<String, ServiceInterface> getRegistry() {
+    return registry;// FIXME should return copy
+  }
+
+  public static ServiceInterface getService(String inName) {
+    return getService(inName, new StaticType<>() {
+    });
+  }
+
+  public static <C extends ServiceConfig, S extends ServiceInterface & ConfigurableService<C>> S getConfigurableService(String inName, StaticType<S> serviceType) {
+    return getService(inName, serviceType);
+  }
+
+  /**
+   * Gets a running service with the specified name. If the name is null or
+   * there's no such service with the specified name, returns null instead.
+   *
+   * @param inName
+   *          The name of the service
+   * @return The service if it exists, or null
+   */
+  @SuppressWarnings("unchecked")
+  public static <S extends ServiceInterface> S getService(String inName, StaticType<S> serviceType) {
+    if (inName == null) {
+      return null;
+    }
+
+    String name = getFullName(inName);
+
+    if (!registry.containsKey(name)) {
+      return null;
+    } else {
+      return (S) registry.get(name);
+    }
+  }
+
+  /**
+   * @return all service names in an array form
+   * 
+   *
+   */
+  static public String[] getServiceNames() {
+    Set<String> ret = registry.keySet();
+    String[] services = new String[ret.size()];
+    if (ret.size() == 0) {
+      return services;
+    }
+
+    // if there are more than 0 services we need runtime
+    // to filter to make sure they are "local"
+    // and this requires a runtime service
+    String localId = Runtime.getInstance().getId();
+    int cnt = 0;
+    for (String fullname : ret) {
+      if (fullname.endsWith(String.format("@%s", localId))) {
+        services[cnt] = CodecUtils.getShortName(fullname);
+      } else {
+        services[cnt] = fullname;
+      }
+      ++cnt;
+    }
+    return services;
+  }
+
+  // Is it a good idea to modify all regex inputs? For example, if the pattern
+  // already contains ".?" then the replacement will result in "..?"
+  // If POSIX-style globs are desired there are different
+  // pattern matching engines designed for that
+  public static boolean match(String text, String pattern) {
+    return text.matches(pattern.replace("?", ".?").replace("*", ".*?"));
+  }
+
+  public static List<String> getServiceNames(String pattern) {
+    return getServices().stream().map(NameProvider::getName).filter(serviceName -> match(serviceName, pattern)).collect(Collectors.toList());
+  }
+
+  /**
+   * @param interfaze
+   *          the interface
+   * @return a list of service names that implement the interface
+   * @throws ClassNotFoundException
+   *           if the class for the requested interface is not found.
+   *
+   */
+  public static List<String> getServiceNamesFromInterface(String interfaze) throws ClassNotFoundException {
+    if (!interfaze.contains(".")) {
+      interfaze = "org.myrobotlab.service.interfaces." + interfaze;
+    }
+
+    return getServiceNamesFromInterface(Class.forName(interfaze));
+  }
+
+  /**
+   * @param interfaze
+   *          interface
+   * @return list of service names
+   * 
+   */
+  public static List<String> getServiceNamesFromInterface(Class<?> interfaze) {
+    return getServicesFromInterface(interfaze).stream().map(ServiceInterface::getFullName).collect(Collectors.toList());
+  }
+
+  /**
+   * Get all currently-running services
+   *
+   * @return A list of all currently-running services
+   */
+  public static List<ServiceInterface> getServices() {
+    return getServices(null);
+  }
+
+  /**
+   * Get all services that belong to an MRL instance with the given ID.
+   * 
+   * @param id
+   *          The ID of the MRL instance
+   * @return A list of the services that belong to the given MRL instance
+   */
+  public static List<ServiceInterface> getServices(String id) {
+    if (id == null) {
+      return new ArrayList<ServiceInterface>(registry.values());
+    }
+
+    List<ServiceInterface> list = new ArrayList<>();
+    // otherwise we are getting services of an instance
+
+    for (String serviceName : registry.keySet()) {
+      ServiceInterface si = registry.get(serviceName);
+      if (si.getId().equals(id)) {
+        list.add(registry.get(serviceName));
+      }
+    }
+    return list;
+  }
+
+  /**
+   * @param interfaze
+   *          interface
+   * @return results
+   *
+   */
+  public ServiceTypeNameResults getServiceTypeNamesFromInterface(String interfaze) {
+    ServiceTypeNameResults results = new ServiceTypeNameResults(interfaze);
+    try {
+
+      if (!interfaze.contains(".")) {
+        interfaze = "org.myrobotlab.service.interfaces." + interfaze;
+      }
+
+      ServiceData sd = ServiceData.getLocalInstance();
+
+      List<MetaData> sts = sd.getServiceTypes();
+
+      for (MetaData st : sts) {
+
+        Set<Class<?>> ancestry = new HashSet<>();
+        Class<?> targetClass = Class.forName(st.getType()); // this.getClass();
+
+        while (targetClass.getCanonicalName().startsWith("org.myrobotlab") && !targetClass.getCanonicalName().startsWith("org.myrobotlab.framework")) {
+          ancestry.add(targetClass);
+          targetClass = targetClass.getSuperclass();
+        }
+
+        for (Class<?> c : ancestry) {
+          Class<?>[] interfaces = Class.forName(c.getName()).getInterfaces();
+          for (Class<?> inter : interfaces) {
+            if (interfaze.equals(inter.getName())) {
+              results.serviceTypes.add(st.getType());
+              break;
+            }
+          }
+        }
+      }
+
+    } catch (Exception e) {
+      error("could not find interfaces for %s - %s %s", interfaze, e.getClass().getSimpleName(), e.getMessage());
+      log.error("getting class", e);
+    }
+
+    return results;
+  }
+
+  /**
+   * return a list of services which are currently running and implement a
+   * specific interface
+   *
+   * @param interfaze
+   *          class
+   * @return list of service interfaces
+   *
+   */
+  // FIXME !!! - use single implementation that gets parents
+  @Deprecated /*
+               * no longer used or needed - change events are pushed no longer
+               * pulled <-- Over complicated solution
+               */
+  public static List<ServiceInterface> getServicesFromInterface(Class<?> interfaze) {
+    synchronized (processLock) {
+      List<ServiceInterface> ret = new ArrayList<ServiceInterface>();
+
+      for (String service : getServiceNames()) {
+        Class<?> clazz = getService(service).getClass();
+        while (clazz != null) {
+          for (Class<?> inter : clazz.getInterfaces()) {
+            if (inter.getName().equals(interfaze.getName())) {
+              ret.add(getService(service));
+              continue;
+            }
+          }
+          clazz = clazz.getSuperclass();
+        }
+      }
+      return ret;
+    }
+  }
+
+  /**
+   * Because startYml is required to be a static variable, since it's needed
+   * "before" a runtime instance exists it will be null in json serialization.
+   * This method is needed so we can serialize the data appropriately.
+   * 
+   * @return
+   */
+  static public StartYml getStartYml() {
+    return startYml;
+  }
+
+  /**
+   * Gets the set of all threads currently running.
+   * 
+   * @return A set containing thread objects representing all running threads
+   */
+  static public Set<Thread> getThreads() {
+    return Thread.getAllStackTraces().keySet();
+  }
+
+  /**
+   * Wraps {@link java.lang.Runtime#totalMemory()}.
+   *
+   * @return The amount of memory available to the JVM in bytes.
+   */
+  public static final long getTotalMemory() {
+
+    return java.lang.Runtime.getRuntime().totalMemory();
+  }
+
+  /**
+   * FIXME - terrible use a uuid
+   * 
+   * unique id's are need for sendBlocking - to uniquely identify the message
+   * this is a method to support that - it is unique within a process, but not
+   * across processes
+   *
+   * @return a unique id
+   */
+  public static final synchronized long getUniqueID() {
+    ++uniqueID;
+    return System.currentTimeMillis();
+  }
+
+  /**
+   * Get how long this MRL instance has been running in human-readable String
+   * form.
+   *
+   * @return The uptime of this instance.
+   */
+  public static String getUptime() {
+    Date now = new Date();
+    Platform platform = Platform.getLocalInstance();
+    String uptime = getDiffTime(now.getTime() - platform.getStartTime().getTime());
+    log.info("up for {}", uptime);
+    return uptime;
+  }
+
+  public static String getPlatformInfo() {
+    Platform platform = Platform.getLocalInstance();
+    StringBuilder sb = new StringBuilder();
+    sb.append(platform.getHostname());
+    sb.append(" ");
+    sb.append(platform.getOS());
+    sb.append(" ");
+    sb.append(platform.getArch());
+    sb.append(".");
+    sb.append(platform.getOsBitness());
+
+    sb.append(" Java ");
+    sb.append(platform.getVmVersion());
+    sb.append(" ");
+    sb.append(platform.getVMName());
+
+    return sb.toString();
+  }
+
+  /**
+   * Get a human-readable String form of a difference in time in milliseconds.
+   *
+   * @param diff
+   *          The difference of time in milliseconds
+   * @return The human-readable string form of the difference in time
+   */
+  public static String getDiffTime(long diff) {
+
+    long diffSeconds = diff / 1000 % 60;
+    long diffMinutes = diff / (60 * 1000) % 60;
+    long diffHours = diff / (60 * 60 * 1000) % 24;
+    long diffDays = diff / (24 * 60 * 60 * 1000);
+
+    StringBuffer sb = new StringBuffer();
+    sb.append(diffDays).append(" days ").append(diffHours).append(" hours ").append(diffMinutes).append(" minutes ").append(diffSeconds).append(" seconds");
+    return sb.toString();
+
+  }
+
+  /**
+   * Get version returns the current version of mrl. It must be done this way,
+   * because the version may be queried on the command line without the desire
+   * to start a "Runtime"
+   *
+   * @return the version of the running platform instance
+   *
+   */
+  public static String getVersion() {
+    return Platform.getLocalInstance().getVersion();
+  }
+
+  /**
+   * Get the latest version number of MRL in String form by querying the public
+   * build server. If it cannot be contacted, this method returns the String
+   * {@code "unknown"}.
+   * 
+   * @return The latest build version in String form
+   */
+  public static String getLatestVersion() {
+    String latest = "https://build.myrobotlab.org:8443/job/myrobotlab/job/develop/lastSuccessfulBuild/buildNumber";
+    byte[] b = Http.get(latest);
+    String version = (b == null) ? "unknown" : "1.1." + new String(b);
+    return version;
+  }
+
+  // FIXME - shouldn't this be in platform ???
+
+  /**
+   * Get the branch that this installation was built from.
+   *
+   * @return The branch
+   * @see Platform#getBranch()
+   */
+  public static String getBranch() {
+    return Platform.getLocalInstance().getBranch();
+  }
+
+  /**
+   * Install all services
+   *
+   * @throws ParseException
+   *           Unknown
+   * @throws IOException
+   *           Unknown
+   */
+  // TODO: Check throws list to see if these are still thrown
+  static public void install() throws ParseException, IOException {
+    install(null, null);
+  }
+
+  /**
+   * Install specified service.
+   *
+   * @param serviceType
+   *          Service to install
+   */
+  static public void install(String serviceType) {
+    install(serviceType, null);
+  }
+
+  /**
+   * Maximum complexity install - allows for blocking and non-blocking install.
+   * During typically runtime install of services - non blocking is desired,
+   * otherwise status info from the install is blocked until installation is
+   * completed. For command line installation "blocking" mode would be desired
+   *
+   * FIXME - problematic in that Runtime.create calls this directly, and this
+   * should be stepped through, because: If we need to install new components, a
+   * restart is likely needed ... we don't do custom dynamic classloaders ....
+   * yet
+   *
+   * License - should be appropriately accepted or rejected by user
+   *
+   * @param serviceType
+   *          the service tyype to install
+   * @param blocking
+   *          if this should block until done.
+   *
+   */
+  static public void install(String serviceType, Boolean blocking) {
+    synchronized (processLock) {
+      Runtime r = getInstance();
+
+      if (blocking == null) {
+        blocking = false;
+      }
+
+      if (installerThread != null) {
+        log.error("another request to install dependencies, 1st request has not completed");
+        return;
+      }
+
+      installerThread = new Thread() {
+        @Override
+        public void run() {
+          try {
+            if (serviceType == null) {
+              r.getRepo().install();
+            } else {
+              r.getRepo().install(serviceType);
+            }
+          } catch (Exception e) {
+            r.error("dependencies failed - install error", e);
+            throw new RuntimeException(String.format("dependencies failed - install error %s", e.getMessage()));
+          }
+        }
+      };
+
+      if (blocking) {
+        installerThread.run();
+      } else {
+        installerThread.start();
+      }
+
+      installerThread = null;
+    }
+  }
+
+  /**
+   * Invoke a service method. The parameter must not be null and must have at
+   * least 2 elements. The first is the service name and the second is the
+   * service method. The rest of the elements are parameters to the specified
+   * method.
+   *
+   * @param invoke
+   *          The array of service name, method, and parameters
+   */
+  static public void invokeCommands(String[] invoke) {
+
+    if (invoke.length < 2) {
+      log.error("invalid invoke request, minimally 2 parameters are required: --invoke service method ...");
+      return;
+    }
+
+    String name = invoke[0];
+    String method = invoke[1];
+
+    // params
+    Object[] data = new Object[invoke.length - 2];
+    for (int i = 2; i < invoke.length; ++i) {
+      data[i - 2] = invoke[i];
+    }
+
+    log.info("attempting to invoke : {}.{}({})\n", name, method, Arrays.toString(data));
+    getInstance().send(name, method, data);
+  }
+
+  /**
+   * Checks if a service is local to this MRL instance. The service must exist.
+   *
+   * @param serviceName
+   *          The name of the service to check
+   * @return Whether the specified service is local or not
+   */
+  public static boolean isLocal(String serviceName) {
+    ServiceInterface sw = getService(serviceName);
+    return Objects.equals(sw.getId(), Runtime.getInstance().getId());
+  }
+
+  /*
+   * check if class is a Runtime class
+   *
+   * @return true if class == Runtime.class
+   */
+  public static boolean isRuntime(Service newService) {
+    return newService.getClass().equals(Runtime.class);
+  }
+
+  /**
+   * Start interactive mode on {@link System#in} and {@link System#out}.
+   *
+   * @see #startInteractiveMode(InputStream, OutputStream)
+   */
+  public void startInteractiveMode() {
+    startInteractiveMode(System.in, System.out);
+  }
+
+  /**
+   * Starts an interactive CLI on the specified input and output streams. The
+   * CLI command processor runs in its own thread and takes commands according
+   * to the CLI API.
+   * 
+   * FIXME - have another shell script which starts jar as ws client with cli
+   * interface Remove this std in/out - it is overly complex and different OSs
+   * handle it differently Windows Java updates have broken it several times
+   *
+   * @param in
+   *          The input stream to take commands from
+   * @param out
+   *          The output stream to print command output to
+   * @return The constructed CLI processor
+   */
+  public InProcessCli startInteractiveMode(InputStream in, OutputStream out) {
+    if (cli != null) {
+      log.info("already in interactive mode");
+      return cli;
+    }
+
+    cli = new InProcessCli(this, "runtime", in, out);
+    Connection c = cli.getConnection();
+    stdCliUuid = (String) c.get("uuid");
+
+    // addRoute(".*", getName(), 100);
+    addConnection(stdCliUuid, cli.getId(), c);
+
+    return cli;
+  }
+
+  /**
+   * Stops interactive mode if it's running.
+   */
+  public void stopInteractiveMode() {
+    if (cli != null) {
+      cli.stop();
+      cli = null;
+    }
+    if (stdCliUuid != null) {
+      removeConnection(stdCliUuid);
+      stdCliUuid = null;
+    }
+  }
+
+  /**
+   * prints help to the console
+   */
+  static void mainHelp() {
+    new CommandLine(new CmdOptions()).usage(System.out);
+  }
+
+  /**
+   * Logs a string message and publishes the message.
+   *
+   * @param msg
+   *          The message to log and publish
+   * @return msg
+   */
+  public static String message(String msg) {
+    getInstance().invoke("publishMessage", msg);
+    log.info(msg);
+    return msg;
+  }
+
+  /**
+   * Listener for state publishing, updates registry
+   * 
+   * @param updatedService
+   *          Updated service to put in the registry
+   */
+  public void onState(ServiceInterface updatedService) {
+    log.info("runtime updating registry info for remote service {}", updatedService.getName());
+    registry.put(String.format("%s@%s", updatedService.getName(), updatedService.getId()), updatedService);
+  }
+
+  public static Registration register(String id, String name, String typeKey, ArrayList<String> interfaces) {
+    synchronized (processLock) {
+      Registration proxy = new Registration(id, name, typeKey, interfaces);
+      register(proxy);
+      return proxy;
+    }
+  }
+
+  /**
+   * Registration is the process where a remote system sends detailed info
+   * related to its services. It will have details on each service type, state,
+   * id, and other info. The registration is serializable, with state
+   * information in a serialized for so that stateless processes or other
+   * non-Java instances can register or be registered.
+   *
+   * Registration might setup subscriptions to support a UI.
+   *
+   * Additional info which will be added in the future is a method map (a
+   * swagger concept) and a list of supported interfaces
+   *
+   * TODO - have rules on what registrations to accept - dependent on security,
+   * desire, re-broadcasting configuration etc. TODO - determine rules on
+   * re-broadcasting based on configuration
+   *
+   * @param registration
+   *          registration
+   * @return registration
+   *
+   */
+  public static Registration register(Registration registration) {
+    synchronized (processLock) {
+      try {
+
+        // TODO - have rules on what registrations to accept - dependent on
+        // security, desire, re-broadcasting configuration etc.
+
+        String fullname = String.format("%s@%s", registration.getName(), registration.getId());
+        if (registry.containsKey(fullname)) {
+          log.info("{} already registered", fullname);
+          return registration;
+        }
+
+        // if (!ForeignProcessUtils.isValidTypeKey(registration.getTypeKey())) {
+        // log.error("Invalid type key being registered: " +
+        // registration.getTypeKey());
+        // return null;
+        // }
+
+        log.info("{}@{} registering at {} of type {}", registration.getName(), registration.getId(), ConfigUtils.getId(), registration.getTypeKey());
+
+        if (!registration.isLocal(ConfigUtils.getId())) {
+
+          // Check if we're registering a java service
+          if (ForeignProcessUtils.isValidJavaClassName(registration.getTypeKey())) {
+
+            String fullTypeName;
+            if (registration.getTypeKey().contains(".")) {
+              fullTypeName = registration.getTypeKey();
+            } else {
+              fullTypeName = String.format("org.myrobotlab.service.%s", registration.getTypeKey());
+            }
+
+            try {
+              // de-serialize, class exists
+              registration.service = Runtime.createService(registration.getName(), fullTypeName, registration.getId());
+              if (registration.getState() != null) {
+                copyShallowFrom(registration.service, CodecUtils.fromJson(registration.getState(), Class.forName(fullTypeName)));
+              }
+            } catch (ClassNotFoundException classNotFoundException) {
+              log.error(String.format("Unknown service class for %s@%s: %s", registration.getName(), registration.getId(), registration.getTypeKey()), classNotFoundException);
+              return null;
+            }
+          } else {
+            // We're registering a foreign process service. We don't need to
+            // check
+            // ForeignProcessUtils.isForeignTypeKey() because the type key is
+            // valid
+            // but is not a java class name
+
+            // Class does not exist, check if registration has empty interfaces
+            // Interfaces should always include ServiceInterface if coming from
+            // remote client
+            if (registration.interfaces == null || registration.interfaces.isEmpty()) {
+              log.error("Unknown service type being registered, registration does not contain any " + "interfaces for proxy generation: " + registration.getTypeKey());
+              return null;
+            }
+
+            // FIXME - probably some more clear definition about the
+            // requirements
+            // of remote
+            // service registration
+            // In general, there should be very few requirements if any, besides
+            // providing a
+            // name, and the proxy
+            // interface should be responsible for creating a minimal
+            // interpretation
+            // (ServiceInterface) for the remote
+            // service
+
+            // Class<?>[] interfaces = registration.interfaces.stream().map(i ->
+            // {
+            // try {
+            // return Class.forName(i);
+            // } catch (ClassNotFoundException e) {
+            // throw new RuntimeException("Unable to load interface " + i + "
+            // defined in remote registration " + registration, e);
+            // }
+            // }).toArray(Class<?>[]::new);
+
+            // registration.service = (ServiceInterface)
+            // Proxy.newProxyInstance(Runtime.class.getClassLoader(),
+            // interfaces,
+            // new ProxyServiceInvocationHandler(registration.getName(),
+            // registration.getId()));
+            try {
+              registration.service = ProxyFactory.createProxyService(registration);
+              log.info("Created proxy: " + registration.service);
+            } catch (Exception e) {
+              // at the moment preventing throw
+              Runtime.getInstance().error(e);
+            }
+          }
+        }
+
+        registry.put(fullname, registration.service);
+
+        if (runtime != null) {
+
+          String type = registration.getTypeKey();
+
+          // If type does not exist in typeToNames, make it an empty hash set
+          // and
+          // return it
+          Set<String> names = runtime.typeToNames.computeIfAbsent(type, k -> new HashSet<>());
+          names.add(fullname);
+
+          // FIXME - most of this could be static as it represents meta data of
+          // class and interfaces
+
+          // FIXME - was false - setting now to true .. because
+          // 1 edge case - "can something fulfill my need of an interface - is
+          // not
+          // currently
+          // switching to true
+          boolean updatedServiceLists = false;
+
+          // maintaining interface type relations
+          // see if this service type is new
+          // PROCESS INDEXES ! - FIXME - will need this in unregister
+          // ALL CLASS/TYPE PROCESSING only needs to happen once per type
+          if (!runtime.serviceTypes.contains(type)) {
+            // CHECK IF "CAN FULFILL"
+            // add the interfaces of the new service type
+            Set<String> interfaces = ClassUtil.getInterfaces(registration.service.getClass(), FILTERED_INTERFACES);
+            for (String interfaze : interfaces) {
+              Set<String> types = runtime.interfaceToType.get(interfaze);
+              if (types == null) {
+                types = new HashSet<>();
+              }
+              types.add(registration.getTypeKey());
+              runtime.interfaceToType.put(interfaze, types);
+            }
+
+            runtime.typeToInterface.put(type, interfaces);
+            runtime.serviceTypes.add(registration.getTypeKey());
+            updatedServiceLists = true;
+          }
+
+          // check to see if any of our interfaces can fulfill requested ones
+          Set<String> myInterfaces = runtime.typeToInterface.get(type);
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).add(fullname);
+              updatedServiceLists = true;
+            }
+          }
+
+          if (updatedServiceLists) {
+            runtime.invoke("publishInterfaceToNames");
+          }
+
+          // TODO - determine rules on re-broadcasting based on configuration
+          runtime.invoke("registered", registration);
+        }
+
+        // TODO - remove ? already get state from registration
+        if (!registration.isLocal(ConfigUtils.getId())) {
+          runtime.subscribe(registration.getFullName(), "publishState");
+        }
+
+      } catch (Exception e) {
+        log.error("registration threw for {}@{}", registration.getName(), registration.getId(), e);
+        return null;
+      }
+
+      return registration;
+    }
+  }
+
+  /**
+   * releases a service - stops the service, its threads, releases its
+   * resources, and removes registry entries
+   *
+   * FIXME - clean up subscriptions from released
+   *
+   * @param inName
+   *          name to release
+   * @return true/false
+   *
+   */
+  public static boolean releaseService(String inName) {
+    ServiceInterface sc = getService(inName);
+    if (sc != null) {
+      sc.releaseService();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Called after any subclassed releaseService has been called, this cleans up
+   * the registry and removes peers
+   * 
+   * @param inName
+   * @return
+   */
+  public static boolean releaseServiceInternal(String inName) {
+    synchronized (processLock) {
+      if (inName == null) {
+        log.debug("release (null)");
+        return false;
+      }
+
+      String name = getFullName(inName);
+
+      String id = CodecUtils.getId(name);
+      if (!id.equals(Runtime.getInstance().getId())) {
+        log.warn("will only release local services - %s is remote", name);
+        return false;
+      }
+
+      log.info("releasing service {}", name);
+
+      if (!registry.containsKey(name)) {
+        log.info("{} not registered", name);
+        return false;
+      }
+
+      // get reference from registry
+      ServiceInterface si = registry.get(name);
+      if (si == null) {
+        log.warn("cannot release {} - not in registry");
+        return false;
+      }
+
+      // FIXME - TODO invoke and or blocking on preRelease - Future
+
+      // send msg to service to self terminate
+      if (si.isLocal()) {
+        si.purgeTasks();
+        si.stopService();
+      } else {
+        if (runtime != null) {
+          runtime.send(name, "releaseService");
+        }
+      }
+
+      // recursive peer release
+      Map<String, Peer> peers = si.getPeers();
+      if (peers != null) {
+        for (Peer peer : peers.values()) {
+          release(peer.name);
+        }
+      }
+
+      // FOR remote this isn't correct - it should wait for
+      // a message from the other runtime to say that its released
+      unregister(name);
+      return true;
+    }
+  }
+
+  /**
+   * Removes registration for a service. Removes the service from
+   * {@link #typeToInterface} and {@link #interfaceToNames}.
+   * 
+   * @param inName
+   *          Name of the service to unregister
+   */
+  public static void unregister(String inName) {
+    synchronized (processLock) {
+      String name = getFullName(inName);
+      log.info("unregister {}", name);
+
+      // get reference from registry
+      ServiceInterface sw = registry.get(name);
+      if (sw == null) {
+        log.debug("{} already unregistered", name);
+        return;
+      }
+
+      // you have to send released before removing from registry
+      if (runtime != null) {
+        runtime.invoke("released", inName); // <- DO NOT CHANGE THIS IS CORRECT
+        // !!
+        // it should be FULLNAME !
+        // runtime.broadcast("released", inName);
+        String type = sw.getTypeKey();
+
+        boolean updatedServiceLists = false;
+
+        // check to see if any of our interfaces can fullfill requested ones
+        Set<String> myInterfaces = runtime.typeToInterface.get(type);
+        if (myInterfaces != null) {
+          for (String inter : myInterfaces) {
+            if (runtime.interfaceToNames.containsKey(inter)) {
+              runtime.interfaceToNames.get(inter).remove(name);
+              updatedServiceLists = true;
+            }
+          }
+        }
+
+        if (updatedServiceLists) {
+          runtime.invoke("publishInterfaceToNames");
+        }
+
+      }
+
+      // FIXME - release autostarted peers ?
+
+      // last step - remove from registry by making new registry
+      // thread safe way
+      Map<String, ServiceInterface> removedService = new TreeMap<>();
+      for (String key : registry.keySet()) {
+        if (!name.equals(key)) {
+          removedService.put(key, registry.get(key));
+        }
+      }
+      registry = removedService;
+
+      // and config
+      RuntimeConfig c = (RuntimeConfig) Runtime.getInstance().config;
+      if (c != null) {
+        c.remove(CodecUtils.getShortName(name));
+      }
+
+      log.info("released {}", name);
+    }
+  }
+
+  /**
+   * Get all remote services.
+   * 
+   * @return List of remote services as proxies
+   */
+  public List<ServiceInterface> getRemoteServices() {
+    return getRemoteServices(null);
+  }
+
+  /**
+   * Get remote services associated with the MRL instance that has the given ID.
+   * 
+   * @param id
+   *          The id of the target MRL instance
+   * @return A list of services running on the target instance
+   */
+  public List<ServiceInterface> getRemoteServices(String id) {
+    List<ServiceInterface> list = new ArrayList<>();
+    for (String serviceName : registry.keySet()) {
+      if (serviceName.contains("@")) {
+        String sid = serviceName.substring(serviceName.indexOf("@") + 1);
+        if (id == null || sid.equals(id)) {
+          list.add(registry.get(serviceName));
+        }
+      }
+    }
+    return list;
+  }
+
+  /**
+   * Releases all local services including Runtime asynchronously.
+   *
+   * @see #releaseAll(boolean, boolean)
+   */
+  public static void releaseAll() {
+    releaseAll(true, false);
+  }
+
+  /**
+   * This does not EXIT(1) !!! releasing just releases all services
+   *
+   * FIXME FIXME FIXME - just call release on each - possibly saving runtime for
+   * last .. send prepareForRelease before releasing
+   *
+   * release all local services
+   *
+   * FIXME - there "should" be an order to releasing the correct way would be to
+   * save the Runtime for last and broadcast all the services being released
+   *
+   * FIXME - send SHUTDOWN event to all running services with a timeout period -
+   * end with System.exit() FIXME normalize with releaseAllLocal and
+   * releaseAllExcept
+   *
+   * local only? YES !!! LOCAL ONLY !!
+   * 
+   * @param releaseRuntime
+   *          Whether the Runtime should also be released
+   */
+  public static void releaseAll(boolean releaseRuntime, boolean block) {
+    // a command thread is issuing this command is most likely
+    // tied to one of the services being removed
+    // therefore this needs to happen asynchronously otherwise
+    // the thread that issued the command will try to destroy/release itself
+    // which almost always causes a deadlock
+    log.debug("releaseAll");
+
+    if (block) {
+      processRelease(releaseRuntime);
+      ConfigUtils.reset();
+    } else {
+
+      new Thread() {
+        @Override
+        public void run() {
+          processRelease(releaseRuntime);
+          ConfigUtils.reset();
+        }
+      }.start();
+
+    }
+  }
+
+  /**
+   * Releases all threads and can be executed in a separate thread.
+   *
+   * @param releaseRuntime
+   *          Whether the Runtime should also be released
+   */
+  static private void processRelease(boolean releaseRuntime) {
+    synchronized (processLock) {
+      // reverse release to order of creation
+      Collection<ServiceInterface> local = getLocalServices().values();
+      List<ServiceInterface> ordered = new ArrayList<>(local);
+      ordered.removeIf(Objects::isNull);
+      Collections.sort(ordered);
+      Collections.reverse(ordered);
+
+      for (ServiceInterface sw : ordered) {
+
+        // no longer needed now - runtime "should be" guaranteed to be last
+        if (sw == Runtime.getInstance()) {
+          // skipping runtime
+          continue;
+        }
+
+        log.info("releasing service {}", sw.getName());
+
+        try {
+          sw.releaseService();
+        } catch (Exception e) {
+          if (runtime != null) {
+            runtime.error("%s threw while releasing", e);
+          }
+          log.error("release", e);
+        }
+      }
+
+      // clean up remote ... the contract should
+      // probably be just remove their references - do not
+      // ask for them to be released remotely ..
+      // in thread safe way
+
+      if (releaseRuntime) {
+        if (runtime != null) {
+          runtime.releaseService();
+        }
+        synchronized (INSTANCE_LOCK) {
+          runtime = null;
+        }
+      } else {
+        // put runtime in new registry
+        Runtime.getInstance();
+        registry = new TreeMap<>();
+        registry.put(runtime.getFullName(), registry.get(runtime.getFullName()));
+      }
+    }
+  }
+
+  /**
+   * Shuts down this instance after the given number of seconds.
+   *
+   * @param seconds
+   *          sets task to shutdown in (n) seconds
+   */
+  // Why is this using the wrapper type? Null can be passed in and cause NPE
+  public static void shutdown(Integer seconds) {
+    log.info("shutting down in {} seconds", seconds);
+    if (seconds > 0) {
+      runtime.addTaskOneShot(seconds * 1000, "shutdown", (Object[]) null);
+      runtime.invoke("publishShutdown", seconds);
+    } else {
+      shutdown();
+    }
+  }
+
+  /**
+   * shutdown terminates the currently running Java virtual machine by
+   * initiating its shutdown sequence. This method never returns normally. The
+   * argument serves as a status code; by convention, a nonzero status code
+   * indicates abnormal termination
+   *
+   */
+  public static void shutdown() {
+    try {
+      log.info("myrobotlab shutting down");
+
+      if (runtime != null) {
+        log.info("stopping interactive mode");
+        runtime.stopInteractiveMode();
+      }
+
+      log.info("pre shutdown on all services");
+      for (ServiceInterface service : getServices()) {
+        service.preShutdown();
+      }
+
+      log.info("releasing all");
+
+      // release
+      releaseAll();
+    } catch (Exception e) {
+      log.error("something threw - continuing to shutdown", e);
+    }
+
+    // calling System.exit(0) before some specialized threads
+    // are completed will actually end up in a deadlock
+    Service.sleep(1000);
+    System.exit(0);
+  }
+
+  public Integer publishShutdown(Integer seconds) {
+    return seconds;
+  }
+
+  /**
+   * publish the folders of the parent directory of configPath if the configPath
+   * is null then publish directory names of data/config
+   *
+   * @return list of configs
+   */
+  public List<String> publishConfigList() {
+    configList = new ArrayList<>();
+
+    File configDirFile = new File(ROOT_CONFIG_DIR);
+    if (!configDirFile.exists() || !configDirFile.isDirectory()) {
+      error("%s config root does not exist", configDirFile.getAbsolutePath());
+      return configList;
+    }
+
+    File[] files = configDirFile.listFiles();
+    if (files == null) {
+      // We checked for if directory earlier, so can only be null for IO error
+      error("IO error occurred while listing config directory files");
+      return configList;
+    }
+    for (File file : files) {
+      String n = file.getName();
+
+      if (!file.isDirectory() || file.isHidden()) {
+        log.info("ignoring {} expecting directory not file", n);
+        continue;
+      }
+
+      configList.add(file.getName());
+    }
+    Collections.sort(configList);
+    return configList;
+  }
+
+  /**
+   * Releases all local services except the services whose names are in the
+   * given set
+   * 
+   * @param saveMe
+   *          The set of services that should not be released
+   */
+  public static void releaseAllServicesExcept(HashSet<String> saveMe) {
+    log.info("releaseAllServicesExcept");
+    List<ServiceInterface> list = Runtime.getServices();
+    for (ServiceInterface si : list) {
+      if (saveMe != null && saveMe.contains(si.getName())) {
+        log.info("leaving {}", si.getName());
+      } else {
+        si.releaseService();
+      }
+    }
+  }
+
+  /**
+   * Release a specific service. Releasing shuts down the service and removes it
+   * from registries.
+   *
+   * @param fullName
+   *          full name The service to be released
+   *
+   */
+  static public void release(String fullName) {
+    releaseService(fullName);
+  }
+
+  /**
+   * Disconnect from remote process. FIXME - not implemented
+   * 
+   * @throws IOException
+   *           Unknown
+   */
+  // FIXME - implement ! also implement the callback events .. onDisconnect
+  public void disconnect() throws IOException {
+    // connect("admin", "ws://localhost:8887/api/messages");
+    log.info("disconnect");
+  }
+
+  /**
+   * FIXME - can this be renamed back to attach ? jump to another process using
+   * the cli
+   *
+   * @param id
+   *          instance id.
+   * @return string
+   *
+   */
+  // FIXME - remove - the way to 'jump' is just to change
+  // context to the correct mrl id e.g. cd /runtime@remote07
+  public String jump(String id) {
+    Connection c = getRoute(stdCliUuid);
+    if (c != null && c.get("cli") != null) {
+      ((InProcessCli) c.get("cli")).setRemote(id);
+    } else {
+      log.error("connection or cli is null for uuid {}", stdCliUuid);
+    }
+
+    return id;
+  }
+
+  /**
+   * Reconnects {@link #cli} to this process.
+   * 
+   * @return The id of this instance
+   */
+  // FIXME - remove ?!?!!?
+  public String exit() {
+    Connection c = getConnection(stdCliUuid);
+    if (c != null && c.get("cli") != null) {
+      ((InProcessCli) c.get("cli")).setRemote(getId());
+    }
+    return getId();
+  }
+
+  /**
+   * Send a command to the {@link InProcessCli}.
+   *
+   * @param srcFullName
+   *          Unknown
+   * @param cmd
+   *          The command to execute
+   */
+  public void sendToCli(String srcFullName, String cmd) {
+    Connection c = getConnection(stdCliUuid);
+    if (c == null || c.get("cli") == null) {
+      log.info("starting interactive mode");
+      startInteractiveMode();
+      sleep(1000);
+    }
+    c = getConnection(stdCliUuid);
+    if (c != null && c.get("cli") != null) {
+      ((InProcessCli) c.get("cli")).process(srcFullName, cmd);
+    } else {
+      log.error("could not start interactive mode");
+    }
+  }
+
+  /**
+   * Connect to the MRL instance at the given URL, auto-reconnecting if
+   * specified and the connection drops.
+   *
+   * FIXME implement autoReconnect
+   *
+   * @param url
+   *          The URL to connect to
+   * @param autoReconnect
+   *          Whether the connection should be re-established if it is dropped
+   */
+  // FIXME - implement
+  public void connect(String url, boolean autoReconnect) {
+    if (!autoReconnect) {
+      connect(url);
+    } else {
+      addTask(1000, "checkConnections");
+    }
+  }
+
+  // FIXME - implement
+  public void checkConnections() {
+    for (Connection connection : connections.values()) {
+      if (connection.containsKey("url")) {
+        /*
+         * FIXME - check on "STATE" ... means we support disconnected
+         * connections .. if (connection.get("url").toString().equals(url)) { //
+         * already connected continue; }
+         */
+      }
+    }
+    // could not find our connection for this "id" - need to reconnect
+    // connect(url);
+  }
+
+  // FIXME -
+  // step 1 - first bind the uuids (1 local and 1 remote)
+  // step 2 - Clients will contain attribute
+  // FIXME - RETRIES TIMEOUTS OTHER COMPLEXITIES
+  // blocking connect - consider a non-blocking thread connect ... e.g.
+  // autoConnect
+
+  /**
+   * Connect to the MRL instance at the given URL
+   * 
+   * @param url
+   *          Where the MRL instance being connected to is located
+   */
+  @Override
+  public void connect(String url) {
+    try {
+
+      // TODO - do auth, ssl and unit tests for them
+      // TODO - get session id
+      // request default describe - on describe do registrations .. zzz
+
+      // standardize request - TODO check for ws wss not http https
+      if (!url.contains("api/messages")) {
+        url += "/api/messages";
+      }
+
+      if (!url.contains("id=")) {
+        url += "?id=" + getId();
+      }
+
+      WsClient client2 = new WsClient();
+      client2.connect(this, url);
+
+      // URI uri = new URI(url);
+      // adding "id" as full url :P ... because we don't know it !!!
+      Connection connection = new Connection(client2.getId(), getId(), getFullName());
+
+      // connection specific
+      connection.put("c-type", "Runtime");
+      // attributes.put("c-endpoint", endpoint);
+      connection.put("c-client", client2);
+
+      // cli specific
+      connection.put("cwd", "/");
+      connection.put("url", url);
+      connection.put("uri", url); // not really correct
+      connection.put("user", "root");
+      connection.put("host", "local");
+
+      // addendum
+      connection.put("User-Agent", "runtime-client");
+
+      addConnection(client2.getId(), url, connection);
+
+      // direct send - may not have and "id" so it will be too runtime vs
+      // runtime@{id}
+      // subscribe to "describe"
+      MRLListener listener = new MRLListener("describe", getFullName(), "onDescribe");
+      Message msg = Message.createMessage(getFullName(), "runtime", "addListener", listener);
+      client2.send(CodecUtils.toJsonMsg(msg));
+
+      // send describe
+      client2.send(CodecUtils.toJsonMsg(getDescribeMsg(null)));
+
+    } catch (Exception e) {
+      log.error("connect to {} giving up {}", url, e.getMessage());
+    }
+  }
+
+  /**
+   * FIXME - this is a gateway callback - probably should be in the gateway
+   * interface - this is a "specific" gateway that supports typeless json or
+   * websockets
+   * <p>
+   * FIXME - decoding should be done at the Connection ! - this should be
+   * onRemoteMessage(msg) !
+   * <p>
+   * callback - from clientRemote - all client connections will recieve here
+   * TODO - get clients directional api - an api per direction incoming and
+   * outgoing
+   *
+   * @param uuid
+   *          - connection for incoming data
+   * @param data
+   *          Incoming message in JSON String form
+   */
+  @Override // uuid
+  public void onRemoteMessage(String uuid, String data) {
+    try {
+
+      // log.debug("connection {} responded with {}", uuid, data);
+      // get api - decode msg - process it
+      Connection connection = getConnection(uuid);
+      if (connection == null) {
+        error("no connection with uuid %s", uuid);
+        return;
+      }
+
+      if (log.isDebugEnabled()) {
+        log.debug("data - [{}]", data);
+      }
+
+      // decoding message envelope
+      Message msg = CodecUtils.fromJson(data, Message.class);
+      log.info("==> {} --> {}.{}", msg.sender, msg.name, msg.method);
+      msg.setProperty("uuid", uuid); // Properties ???? REMOVE ???
+
+      if (msg.containsHop(getId())) {
+        log.error("{} dumping duplicate hop msg to avoid cyclical from {} --to--> {}.{} | {}", getName(), msg.sender, msg.name, msg.method, msg.getHops());
+        return;
+      }
+
+      addRoute(msg.getSrcId(), uuid, 10);
+
+      // add our id - we don't want to see it again
+      msg.addHop(getId());
+
+      Object ret = null;
+
+      // FIXME - see if same code block exists in WebGui .. normalize
+      if (isLocal(msg)) {
+
+        // log.info("--> {}.{} from {}", msg.name, msg.method, msg.sender);
+
+        String serviceName = msg.getName();
+        // to decode fully we need class name, method name, and an array of json
+        // encoded parameters
+        MethodCache cache = MethodCache.getInstance();
+        Class<?> clazz = Runtime.getClass(serviceName);
+        if (clazz == null) {
+          log.error("local msg but no Class for requested service {}", serviceName);
+          return;
+        }
+        Object[] params = cache.getDecodedJsonParameters(clazz, msg.method, msg.data);
+
+        Method method = cache.getMethod(clazz, msg.method, params);
+        ServiceInterface si = Runtime.getService(serviceName);
+        if (method == null) {
+          log.error("cannot find {}", cache.makeKey(clazz, msg.method, cache.getParamTypes(params)));
+          return;
+        }
+        if (si == null) {
+          log.error("si null for serviceName {}", serviceName);
+          return;
+        }
+
+        ret = method.invoke(si, params);
+
+        // propagate return data to subscribers
+        si.out(msg.method, ret);
+
+      } else {
+        log.info("GATEWAY {} RELAY {} --to--> {}.{}", getName(), msg.sender, msg.name, msg.method);
+        send(msg);
+      }
+
+    } catch (Exception e) {
+      log.error("processing msg threw", e);
+    }
+  }
+
+  /**
+   * Add a route to the route table
+   *
+   * @param remoteId
+   *          Id of the remote instance
+   * @param uuid
+   *          Unknown
+   * @param metric
+   *          Unknown
+   * @see RouteTable#addRoute(String, String, int)
+   */
+  public void addRoute(String remoteId, String uuid, int metric) {
+    routeTable.addRoute(remoteId, uuid, metric);
+  }
+
+  /**
+   * Start Runtime with the specified config
+   *
+   * @param configName
+   *          The name of the config file
+   */
+  static public void startConfig(String configName) {
+    setConfig(configName);
+    Runtime runtime = Runtime.getInstance();
+    runtime.processingConfig = true; // multiple inbox threads not available
+    runtime.invoke("publishConfigStarted", configName);
+    RuntimeConfig rtConfig = runtime.readServiceConfig(runtime.getConfigName(), "runtime", new StaticType<>() {
+    });
+    if (rtConfig == null) {
+      runtime.error("cannot find %s%s%s", runtime.getConfigName(), fs, "runtime.yml");
+      return;
+    }
+
+    runtime.apply(rtConfig);
+
+    Plan plan = new Plan("runtime");
+    // for every service listed in runtime registry - load it
+    // FIXME - regex match on filesystem matches on *.yml
+    for (String service : rtConfig.getRegistry()) {
+
+      if ("runtime".equals(service) || Runtime.isStarted(service)) {
+        continue;
+      }
+
+      // has to be loaded
+      File file = new File(Runtime.ROOT_CONFIG_DIR + fs + runtime.getConfigName() + fs + service + ".yml");
+      if (!file.exists()) {
+        runtime.error("cannot read file %s - skipping", file.getPath());
+        continue;
+      }
+
+      ServiceConfig sc = runtime.readServiceConfig(runtime.getConfigName(), service);
+      try {
+        if (sc == null) {
+          continue;
+        }
+        runtime.loadService(plan, service, sc.type, true, 0);
+      } catch (Exception e) {
+        runtime.error(e);
+      }
+    }
+
+    // for all newly created services start them
+    Map<String, ServiceInterface> created = Runtime.createServicesFromPlan(plan, null, null);
+    for (ServiceInterface si : created.values()) {
+      si.startService();
+    }
+
+    runtime.processingConfig = false; // multiple inbox threads not available
+    runtime.invoke("publishConfigFinished", configName);
+
+  }
+
+  public String publishConfigStarted(String configName) {
+    log.info("publishConfigStarted {}", configName);
+    // Make Note: done inline, because the thread actually doing the config
+    // processing
+    // would need to be finished with it before this thread could be invoked
+    // if multiple inbox threads were available then this would be possible
+    // processingConfig = true;
+    return configName;
+  }
+
+  public String publishConfigFinished(String configName) {
+    log.info("publishConfigFinished {}", configName);
+    // Make Note: done inline, because the thread actually doing the config
+    // processing
+    // would need to be finished with it before this thread could be invoked
+    // if multiple inbox threads were available then this would be possible
+    // processingConfig = false;
+    return configName;
+  }
+
+  /**
+   * Start a service of the specified type as the specified name.
+   *
+   * @param name
+   *          The name of the new service
+   * @param type
+   *          The type of the new service
+   * @return The started service
+   */
+  static public ServiceInterface start(String name, String type) {
+    synchronized (processLock) {
+      try {
+
+        ServiceInterface requestedService = Runtime.getService(name);
+        if (requestedService != null) {
+          log.info("requested service already exists");
+          if (requestedService.isRunning()) {
+            log.info("requested service already running");
+          } else {
+            requestedService.startService();
+          }
+          return requestedService;
+        }
+
+        Plan plan = Runtime.load(name, type);
+
+        Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+
+        if (services == null) {
+          Runtime.getInstance().error("cannot create instance of %s with type %s given current configuration", name, type);
+          return null;
+        }
+
+        requestedService = Runtime.getService(name);
+
+        // FIXME - does some order need to be maintained e.g. all children
+        // before
+        // parent
+        // breadth first, depth first, external order ordinal ?
+        for (ServiceInterface service : services.values()) {
+          if (service.getName().equals(name)) {
+            continue;
+          }
+          if (!Runtime.isStarted(service.getName())) {
+            service.startService();
+          }
+        }
+
+        if (requestedService == null) {
+          Runtime.getInstance().error("could not start %s of type %s", name, type);
+          return null;
+        }
+
+        // getConfig() was problematic here for JMonkeyEngine
+        ServiceConfig sc = requestedService.getConfig();
+        // Map<String, Peer> peers = sc.getPeers();
+        // if (peers != null) {
+        // for (String p : peers.keySet()) {
+        // Peer peer = peers.get(p);
+        // log.info("peer {}", peer);
+        // }
+        // }
+        // recursive - start peers of peers of peers ...
+        Map<String, Peer> subPeers = sc.getPeers();
+        if (sc != null && subPeers != null) {
+          for (String subPeerKey : subPeers.keySet()) {
+            // IF AUTOSTART !!!
+            Peer subPeer = subPeers.get(subPeerKey);
+            if (subPeer.autoStart) {
+              Runtime.start(sc.getPeerName(subPeerKey), subPeer.type);
+            }
+          }
+        }
+
+        requestedService.startService();
+        return requestedService;
+      } catch (Exception e) {
+        runtime.error(e);
+      }
+      return null;
+    }
+  }
+
+  /**
+   * single parameter name info supplied - potentially all information regarding
+   * this service could be found in on the filesystem or in the plan
+   * 
+   * @param name
+   * @return
+   */
+  static public ServiceInterface start(String name) {
+    synchronized (processLock) {
+      if (Runtime.getService(name) != null) {
+        // already exists
+        ServiceInterface si = Runtime.getService(name);
+        if (!si.isRunning()) {
+          si.startService();
+        }
+        return si;
+      }
+      Plan plan = Runtime.load(name, null);
+      Map<String, ServiceInterface> services = createServicesFromPlan(plan, null, name);
+      // FIXME - order ?
+      for (ServiceInterface service : services.values()) {
+        service.startService();
+      }
+      return Runtime.getService(name);
+    }
+  }
+
+  public static Plan load(String name, String type) {
+    synchronized (processLock) {
+      try {
+        Runtime runtime = Runtime.getInstance();
+        return runtime.loadService(new Plan("runtime"), name, type, true, 0);
+      } catch (IOException e) {
+        runtime.error(e);
+      }
+      return null;
+    }
+  }
+
+  /**
+   * Construct a new Runtime with the given name and ID. The name should always
+   * be "runtime" as parts of interprocess communication assume it to be so.
+   *
+   * TODO Check if there's a way to remove the assumptions about Runtime's name
+   * 
+   * @param n
+   *          Name of the runtime. Should always be {@code "runtime"}
+   * @param id
+   *          The ID of the instance this runtime belongs to.
+   */
+  public Runtime(String n, String id) {
+    super(n, id);
+
+    // because you need to start with something ...
+    config = new RuntimeConfig();
+
+    repo = (IvyWrapper) Repo.getInstance(LIBRARIES, "IvyWrapper");
+
+    /**
+     * This is used to run through all the possible services and determine if
+     * they have any missing dependencies. If they do not they become
+     * "installed". The installed flag makes the gui do a crossout when a
+     * service type is selected.
+     */
+    for (MetaData metaData : serviceData.getServiceTypes()) {
+      Set<ServiceDependency> deps = repo.getUnfulfilledDependencies(metaData.getType());
+      if (deps.size() == 0) {
+        metaData.installed = true;
+      } else {
+        log.info("{} not installed", metaData.getSimpleName());
+      }
+    }
+
+    setLocale(Locale.getDefault().getTag());
+    locales = Locale.getDefaults();
+
+    if (runtime.platform == null) {
+      runtime.platform = Platform.getLocalInstance();
+    }
+
+    // setting the id and the platform
+    platform = Platform.getLocalInstance();
+
+    String libararyPath = System.getProperty("java.library.path");
+    String userDir = System.getProperty("user.dir");
+    String userHome = System.getProperty("user.home");
+
+    // initialize the config list
+    publishConfigList();
+
+    // TODO this should be a single log statement
+    // http://developer.android.com/reference/java/lang/System.html
+
+    String format = "yyyy/MM/dd HH:mm:ss";
+    SimpleDateFormat sdf = new SimpleDateFormat(format);
+    SimpleDateFormat gmtf = new SimpleDateFormat(format);
+    gmtf.setTimeZone(TimeZone.getTimeZone("UTC"));
+    log.info("============== args begin ==============");
+    StringBuffer sb = new StringBuffer();
+
+    jvmArgs = getJvmArgs();
+    if (globalArgs != null) {
+      for (int i = 0; i < globalArgs.length; ++i) {
+        sb.append(globalArgs[i]);
+      }
+    }
+    if (jvmArgs != null) {
+      log.info("jvmArgs {}", Arrays.toString(jvmArgs.toArray()));
+    }
+    log.info("file.encoding {}", System.getProperty("file.encoding"));
+    log.info("args {}", Arrays.toString(globalArgs));
+
+    log.info("============== args end ==============");
+
+    log.info("============== env begin ==============");
+
+    Map<String, String> env = System.getenv();
+    if (env.containsKey("PATH")) {
+      log.info("PATH={}", env.get("PATH"));
+    } else {
+      log.info("PATH not defined");
+    }
+    if (env.containsKey("JAVA_HOME")) {
+      log.info("JAVA_HOME={}", env.get("JAVA_HOME"));
+    } else {
+      log.info("JAVA_HOME not defined");
+    }
+
+    // also look at bitness detection in framework.Platform
+    String procArch = env.get("PROCESSOR_ARCHITECTURE");
+    String procArchWow64 = env.get("PROCESSOR_ARCHITEW6432");
+    if (procArch != null) {
+      log.info("PROCESSOR_ARCHITECTURE={}", procArch);
+    } else {
+      log.info("PROCESSOR_ARCHITECTURE not defined");
+    }
+    if (procArchWow64 != null) {
+      log.info("PROCESSOR_ARCHITEW6432={}", procArchWow64);
+    } else {
+      log.info("PROCESSOR_ARCHITEW6432 not defined");
+    }
+    log.info("============== env end ==============");
+
+    log.info("============== platform ==============");
+    long startTime = platform.getStartTime().getTime();
+    log.info("{} - GMT - {}", sdf.format(startTime), gmtf.format(startTime));
+    log.info("pid {}", platform.getPid());
+    log.info("hostname {}", platform.getHostname());
+    log.info("ivy [runtime,{}.{}.{}]", platform.getArch(), platform.getJvmBitness(), platform.getOS());
+    log.info("version {} branch {} commit {} build {}", platform.getVersion(), platform.getBranch(), platform.getCommit(), platform.getBuild());
+    System.out.println(String.format("version %s branch %s commit %s build %s", platform.getVersion(), platform.getBranch(), platform.getCommit(), platform.getBuild()));
+    log.info("platform manifest {}", Platform.getManifest());
+    log.info("platform [{}}]", platform);
+    log.info("version [{}]", platform.getVersion());
+    log.info("root [{}]", FileIO.getRoot());
+    log.info("cfg dir [{}]", FileIO.getCfgDir());
+    log.info("sun.arch.data.model [{}]", System.getProperty("sun.arch.data.model"));
+
+    log.info("============== non-normalized ==============");
+    log.info("os.name [{}] getOS [{}]", System.getProperty("os.name"), platform.getOS());
+    log.info("os.arch [{}] getArch [{}]", System.getProperty("os.arch"), platform.getArch());
+    log.info("os.version [{}]", System.getProperty("os.version"));
+
+    log.info("java.vm.name [{}]", System.getProperty("java.vm.name"));
+    log.info("java.vm.vendor [{}]", System.getProperty("java.vm.vendor"));
+    log.info("java.specification.version [{}]", System.getProperty("java.specification.version"));
+
+    String vmVersion = System.getProperty("java.specification.version");
+    vmVersion = "11";
+    if ("1.8".equals(vmVersion)) {
+      error("Unsupported Java %s - please remove version and install Java 1.8", vmVersion);
+    }
+
+    // test ( force encoding )
+    // System.setProperty("file.encoding","UTF-8" );
+    log.info("file.encoding [{}]", System.getProperty("file.encoding"));
+    log.info("Charset.defaultCharset() [{}]", Charset.defaultCharset());
+    log.info("user.language [{}]", System.getProperty("user.language"));
+    log.info("user.country [{}]", System.getProperty("user.country"));
+    log.info("user.variant [{}]", System.getProperty("user.variant"));
+
+    // System.getProperty("pi4j.armhf")
+
+    log.info("java.home [{}]", System.getProperty("java.home"));
+    log.debug("java.class.path [{}]", System.getProperty("java.class.path"));
+    log.info("java.library.path [{}]", libararyPath);
+    log.info("user.dir [{}]", userDir);
+
+    log.info("user.home [{}]", userHome);
+    log.info("total mem [{}] Mb", Runtime.getTotalMemory() / 1048576);
+    log.info("total free [{}] Mb", Runtime.getFreeMemory() / 1048576);
+    // Access restriction - log.info("total physical mem [{}] Mb",
+    // Runtime.getTotalPhysicalMemory() / 1048576);
+
+    if (platform.isWindows()) {
+      log.info("guessed os bitness [{}]", platform.getOsBitness());
+      // try to compare os bitness with jvm bitness
+      if (platform.getOsBitness() != platform.getJvmBitness()) {
+        log.warn("detected possible bitness mismatch between os & jvm");
+      }
+    }
+
+    log.info("getting local repo");
+
+    if (repo != null)/* transient */ {
+      repo.addStatusPublisher(this);
+    }
+  }
+
+  /**
+   * Get the process ID of the current JVM.
+   *
+   * @return The process ID.
+   * @see Platform#getPid()
+   */
+  public String getPid() {
+    return Platform.getLocalInstance().getPid();
+  }
+
+  public String publishDefaultRoute(String defaultRoute) {
+    return defaultRoute;
+  }
+
+  /**
+   * Get the hostname of the computer this instance is running on.
+   * 
+   * @return The computer's hostname
+   * @see Platform#getHostname()
+   */
+  public String getHostname() {
+    return Platform.getLocalInstance().getHostname();
+  }
+
+  /**
+   * publishing event - since checkForUpdates may take a while
+   */
+  public void checkingForUpdates() {
+    log.info("checking for updates");
+  }
+
+  /**
+   * Read an entire input stream as a string and return it. If the input stream
+   * does not have any more tokens, returns an empty string instead.
+   *
+   * @param is
+   *          The input stream to read from
+   * @return The entire input stream read as a string
+   */
+  static public String getInputAsString(InputStream is) {
+    try (java.util.Scanner s = new java.util.Scanner(is)) {
+      return s.useDelimiter("\\A").hasNext() ? s.next() : "";
+    }
+  }
+
+  /**
+   * list the contents of the current working directory
+   *
+   * @return object
+   */
+  public Object ls() {
+    return ls(null, null);
+  }
+
+  /**
+   * List the contents of an absolute path.
+   *
+   * @param path
+   *          The path to list
+   * @return The contents of the directory
+   */
+  public Object ls(String path) {
+    return ls(null, path);
+  }
+
+  /**
+   * list the contents of a specific path
+   * <p>
+   * </p>
+   * TODO It looks like this only returns Object because it wants to return
+   * either a String array or a method entry list. It would probably be best to
+   * just convert the method entry list to a string array using streams and
+   * change the signature to match.
+   *
+   * @param contextPath
+   *          c
+   * @param path
+   *          p
+   * @return object
+   *
+   */
+  public Object ls(String contextPath, String path) {
+    String absPath = null;
+
+    if (contextPath != null) {
+      path = contextPath + path;
+    }
+
+    if (path == null) {
+      path = "/";
+    }
+
+    // ALL SHOULD BE ABSOLUTE PATH AT THIS POINT
+    // IE STARTING WITH /
+
+    if (!path.startsWith("/")) {
+      path = "/" + path;
+    }
+
+    absPath = path;
+
+    String[] parts = absPath.split("/");
+
+    String ret = null;
+    if (absPath.equals("/")) {
+      return Runtime.getServiceNames();
+    } else if (parts.length == 2 && !absPath.endsWith("/")) {
+      return Runtime.getService(parts[1]);
+    } else if (parts.length == 2 && absPath.endsWith("/")) {
+      ServiceInterface si = Runtime.getService(parts[1]);
+      if (si == null) {
+        return null;
+      }
+      return si.getDeclaredMethodNames();
+      /*
+       * } else if (parts.length == 3 && !absPath.endsWith("/")) { // execute 0
+       * parameter function ??? return Runtime.getService(parts[1]);
+       */
+    } else if (parts.length == 3) {
+      ServiceInterface si = Runtime.getService(parts[1]);
+      MethodCache cache = MethodCache.getInstance();
+      List<MethodEntry> me = cache.query(si.getTypeKey(), parts[2]);
+      return me; // si.getMethodMap().get(parts[2]);
+    }
+    return ret;
+  }
+
+  /**
+   * serviceName at id
+   *
+   * @return runtime name with instance id.
+   *
+   */
+  public String whoami() {
+    return "runtime@" + getId();
+  }
+
+  // end cli commands ----
+
+  // ---------- Java Runtime wrapper functions begin --------
+  /**
+   * Executes the specified command and arguments in a separate process. Returns
+   * the exit value for the subprocess.
+   *
+   * @param program
+   *          The name of or path to an executable program. If given a name, the
+   *          program must be on the system PATH.
+   * @return The exit value of the subprocess
+   */
+  static public String exec(String program) {
+    return execute(program, null, null, null, null);
+  }
+
+  /*
+   * FIXME - see if this is used anymore publishing point of Ivy sub system -
+   * sends event failedDependency when the retrieve report for a Service fails
+   */
+  @Deprecated /* remove */
+  public String failedDependency(String dep) {
+    return dep;
+  }
+
+  public static Platform getPlatform() {
+    return getInstance().platform;
+  }
+
+  // FIXME - should be removed - use Platform.getLocalInstance().is64bit()
+  @Deprecated
+  public boolean is64bit() {
+    return getInstance().platform.getJvmBitness() == 64;
+  }
+
+  public Repo getRepo() {
+    return repo;
+  }
+
+  /**
+   * Returns an array of all the simple type names of all the possible services.
+   * The data originates from the repo's serviceData.json file.
+   * <p>
+   * There is a local one distributed with the installation jar. When an
+   * "update" is forced, MRL will try to download the latest copy from the repo.
+   * <p>
+   * The serviceData.json lists all service types, dependencies, categories and
+   * other relevant information regarding service creation
+   *
+   * @return list of all service type names
+   */
+  public String[] getServiceTypeNames() {
+    return getServiceTypeNames("all");
+  }
+
+  /**
+   * getServiceTypeNames will publish service names based on some filter
+   * criteria
+   *
+   * @param filter
+   *          f
+   * @return array of service types
+   *
+   */
+  public String[] getServiceTypeNames(String filter) {
+    return serviceData.getServiceTypeNames(filter);
+  }
+
+  // FIXME THIS IS NOT NORMALIZED !!!
+
+  /**
+   * Send the full log of the currently running MRL instance to the MyRobotLab
+   * developers for help. The userID is the name of the MyRobotLab.org user
+   * account
+   * 
+   * @param userId
+   *          Name of the MRL website account to link the log to
+   * @return Whether the log was sent successfully, info if yes and error if no.
+   */
+  static public Status noWorky(String userId) {
+    Status status = null;
+    try {
+      String retStr = HttpRequest.postFile("http://noworky.myrobotlab.org/no-worky", userId, "file", new File(LoggingFactory.getLogFileName()));
+      if (retStr.contains("Upload:")) {
+        log.info("noWorky successfully sent - our crack team of experts will check it out !");
+        status = Status.info("no worky sent");
+      } else {
+        status = Status.error("could not send");
+      }
+    } catch (Exception e) {
+      log.error("the noWorky didn't worky !");
+      status = Status.error(e);
+    }
+
+    // this makes the 'static' of this method pointless
+    // perhaps the webgui should invoke rather than call directly .. :P
+    Runtime.getInstance().invoke("publishNoWorky", status);
+    return status;
+  }
+
+  static public Status publishNoWorky(Status status) {
+    return status;
+  }
+
+  // FIXME - create interface for this
+  public String publishMessage(String msg) {
+    return msg;
+  }
+
+  @Override
+  @Deprecated /* use onResponse ??? */
+  public void onMessage(Message msg) {
+    // TODO: what do we do when we get a message?
+    log.info("onMessage()");
+  }
+
+  /**
+   * Publishing point when a service was successfully registered locally -
+   * regardless if the service is local or not.
+   *
+   * TODO - more business logic can be created here to limit broadcasting or
+   * re-broadcasting published registrations
+   *
+   * @param registration
+   *          - contains all the information need for a registration to process
+   */
+  @Override
+  public Registration registered(Registration registration) {
+    return registration;
+  }
+
+  /**
+   * released event - when a service is successfully released from the registry
+   * this event is triggered
+   *
+   */
+  @Override
+  public String released(String name) {
+    return name;
+  }
+
+  /**
+   * A function for runtime to "save" a service - or if the service does not
+   * exists save the "default" config of that type of service
+   *
+   * @param name
+   *          name of service to export
+   * @return true/false
+   * @throws IOException
+   *           boom
+   *
+   */
+  @Deprecated /* use save(name) */
+  public boolean export(String name /* , String type */) throws IOException {
+    return save(name);
+  }
+
+  public boolean save(String name /* , String type */) throws IOException {
+    ServiceInterface si = getService(name);
+    if (si != null) {
+      return si.save();
+    }
+    error("cannot save %s - does not exist", name);
+    return false;
+  }
+
+  /**
+   * restart occurs after applying updates - user or config data needs to be
+   * examined and see if its an appropriate time to restart - if it is the
+   * spawnBootstrap method will be called and bootstrap.jar will go through its
+   * sequence to update myrobotlab.jar
+   */
+  public void restart() {
+    // to avoid deadlock of shutting down from external messages
+    // we spawn a kill thread
+    new Thread("kill-thread") {
+      @Override
+      public void run() {
+        try {
+
+          info("restarting");
+
+          // FIXME - should we save() load() ???
+          // export("last-restart");
+
+          // shutdown all services process - send ready to shutdown - ask back
+          // release all services
+          for (ServiceInterface service : getServices()) {
+            service.preShutdown();
+          }
+
+          // check if ready ???
+
+          // release all local services
+          releaseAll();
+
+          if (runtime != null) {
+            runtime.releaseService();
+          }
+
+          // make sure python is included
+          // options.services.add("python");
+          // options.services.add("Python");
+
+          // force invoke
+          // options.invoke = new String[] { "python", "execFile",
+          // "lastRestart.py" };
+
+          // create builder from Launcher daemonize ?
+          log.info("re launching with commands \n{}", CmdOptions.toString(options.getOutputCmd()));
+          ProcessBuilder pb = Launcher.createBuilder(options);
+
+          // fire it off
+          Process restarted = pb.start();
+          // it "better" not be a requirement that a process must consume its
+          // std streams
+          // "hopefully" - if the OS realizes the process is dead it moves the
+          // streams to /dev/null ?
+          // StreamGobbler gobbler = new
+          // StreamGobbler(String.format("%s-gobbler", getName()),
+          // restarted.getInputStream());
+          // gobbler.start();
+
+          // dramatic pause
+          sleep(2000);
+
+          // check if process exists
+          if (restarted.isAlive()) {
+            log.info("yay! we continue to live in future generations !");
+          } else {
+            log.error("omg! ... I killed all the services and now there is no offspring ! :(");
+          }
+          log.error("goodbye ...");
+          shutdown();
+        } catch (Exception e) {
+          log.error("shutdown threw", e);
+        }
+      }
+    }.start();
+  }
+
+  /**
+   * Get the META-INF/MANIFEST.MF file from the myrobotlab.jar as String
+   * key-value pairs.
+   * 
+   * @return key-value pairs contained in the manifest file
+   * @see Platform#getManifest()
+   */
+  static public Map<String, String> getManifest() {
+    return Platform.getManifest();
+  }
+
+  /**
+   * Runtime's setLogLevel will set the root log level if its called from a
+   * service - it will only set that Service type's log level
+   *
+   * @param level
+   *          - DEBUG | INFO | WARN | ERROR
+   * @return the level which was set
+   */
+  static public String setLogLevel(String level) {
+    log.info("setLogLevel {}", level);
+    Logging logging = LoggingFactory.getInstance();
+    logging.setLevel(level);
+    log.info("setLogLevel {}", level);
+    return level;
+  }
+
+  /**
+   * Get the log level of this MRL instance
+   *
+   * @return The log level as a String.
+   * @see Logging#getLevel()
+   */
+  static public String getLogLevel() {
+    Logging logging = LoggingFactory.getInstance();
+    return logging.getLevel();
+  }
+
+  /**
+   * Set the file to output logs to. This will remove all previously-applied
+   * appenders from the logging system.
+   *
+   * @param file
+   *          The file to output logs to
+   * @return file
+   * @see Logging#removeAllAppenders()
+   */
+  static public String setLogFile(String file) {
+    log.info("setLogFile {}", file);
+    Logging logging = LoggingFactory.getInstance();
+    logging.removeAllAppenders();
+    LoggingFactory.setLogFile(file);
+    logging.addAppender(AppenderType.FILE);
+    return file;
+  }
+
+  /**
+   * Disables logging by removing all appenders. To re-enable call
+   * {@link #setLogFile(String)} or add appenders.
+   *
+   * @see Logging#addAppender(String)
+   */
+  static public void disableLogging() {
+    Logging logging = LoggingFactory.getInstance();
+    logging.removeAllAppenders();
+  }
+
+  /**
+   * Stops all service-related running items. This releases the singleton
+   * referenced by this class, but it does not guarantee that the old service
+   * will be GC'd. FYI - if stopServices does not remove INSTANCE - it is not
+   * re-entrant in junit tests
+   */
+  @Override
+  public void releaseService() {
+    if (runtime != null) {
+      runtime.purgeTasks();
+      runtime.stopService();
+      runtime.stopInteractiveMode();
+      runtime.getRepo().removeStatusPublishers();
+      if (cli != null) {
+        cli.stop();
+      }
+      registry = new TreeMap<>();
+    }
+    synchronized (INSTANCE_LOCK) {
+      runtime = null;
+    }
+  }
+
+  /**
+   * Close all connections using this runtime as the gateway. This includes both
+   * inbound and outbound connections.
+   */
+  public void closeConnections() {
+    for (Connection c : connections.values()) {
+      String gateway = c.getGateway();
+      if (getFullName().equals(gateway)) {
+        WsClient client = (WsClient) c.get("c-client");
+        client.close();
+      }
+    }
+  }
+
+  // FYI - the way to call "all" service methods !
+
+  /**
+   * Clear all services' last error.
+   * 
+   * @see ServiceInterface#clearLastError()
+   */
+  public void clearErrors() {
+    for (String serviceName : registry.keySet()) {
+      send(serviceName, "clearLastError");
+    }
+  }
+
+  /**
+   * Check if any services have errors.
+   *
+   * @return Whether any service has an error
+   * @see ServiceInterface#hasError()
+   */
+  public static boolean hasErrors() {
+    for (ServiceInterface si : registry.values()) {
+      if (si.hasError()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * remove all subscriptions from all local Services
+   */
+  static public void removeAllSubscriptions() {
+    for (ServiceInterface si : getLocalServices().values()) {
+      List<String> nlks = si.getNotifyListKeySet();
+      for (int i = 0; i < nlks.size(); ++i) {
+        si.getNotifyList().clear();
+      }
+    }
+  }
+
+  /**
+   * Get recent errors from all local services.
+   * 
+   * @return A list of most recent service errors
+   * @see ServiceInterface#getLastError()
+   */
+  public static List<Status> getErrors() {
+    ArrayList<Status> stati = new ArrayList<Status>();
+    for (ServiceInterface si : getLocalServices().values()) {
+      Status status = si.getLastError();
+      if (status != null && status.isError()) {
+        log.info(status.toString());
+        stati.add(status);
+      }
+    }
+    return stati;
+  }
+
+  /**
+   * Broadcast the states of all local services.
+   */
+  public static void broadcastStates() {
+    for (ServiceInterface si : getLocalServices().values()) {
+      si.broadcastState();
+    }
+  }
+
+  /**
+   * Get the Runtime singleton instance.
+   * 
+   * @return The singleton instance
+   * @see #getInstance()
+   */
+  public static Runtime get() {
+    return Runtime.getInstance();
+  }
+
+  /**
+   * Execute an external program with arguments if specified. args must not be
+   * null and the length must be greater than zero, the first element is the
+   * program to be executed. If the program is just a name and not a path to the
+   * executable then it must be on the operating system PATH.
+   *
+   * @see <a href=
+   *      "https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them">
+   *      What are PATH and other environment variables?</a>
+   * @param args
+   *          The program to be executed as the first element and the args to
+   *          the program as the rest, if any
+   * @return The program's stdout and stderr output
+   */
+  static public String execute(String... args) {
+    if (args == null || args.length == 0) {
+      log.error("execute invalid number of args");
+      return null;
+    }
+    String program = args[0];
+    List<String> list = null;
+
+    if (args.length > 1) {
+      list = new ArrayList<String>();
+      for (int i = 1; i < args.length; ++i) {
+        list.add(args[i]);
+      }
+    }
+
+    return execute(program, list, null, null, true);
+  }
+
+  /**
+   * Execute an external program with a list of arguments, a specified working
+   * directory, any additional environment variables, and whether the execution
+   * blocks.
+   *
+   * TODO Implement workingDir and block
+   *
+   * @param program
+   *          The program to be executed
+   * @param args
+   *          Any arguments to the command
+   * @param workingDir
+   *          The directory to execute the program in
+   * @param additionalEnv
+   *          Any additional environment variables
+   * @param block
+   *          Whether this method blocks for the program to execute
+   * @return The programs stderr and stdout output
+   */
+
+  static public String execute(String program, List<String> args, String workingDir, Map<String, String> additionalEnv, boolean block) {
+    log.debug("execToString(\"{} {}\")", program, args);
+
+    List<String> command = new ArrayList<>();
+    command.add(program);
+    if (args != null) {
+      command.addAll(args);
+    }
+
+    ProcessBuilder builder = new ProcessBuilder(command);
+    if (workingDir != null) {
+      builder.directory(new File(workingDir));
+    }
+
+    Map<String, String> environment = builder.environment();
+    if (additionalEnv != null) {
+      environment.putAll(additionalEnv);
+    }
+
+    StringBuilder outputBuilder = new StringBuilder();
+
+    try {
+      Process handle = builder.start();
+
+      InputStream stdErr = handle.getErrorStream();
+      InputStream stdOut = handle.getInputStream();
+
+      // Read the output streams in separate threads to avoid potential blocking
+      Thread stdErrThread = new Thread(() -> readStream(stdErr, outputBuilder));
+      stdErrThread.start();
+
+      Thread stdOutThread = new Thread(() -> readStream(stdOut, outputBuilder));
+      stdOutThread.start();
+
+      if (block) {
+        int exitValue = handle.waitFor();
+        outputBuilder.append("Exit Value: ").append(exitValue);
+        log.info("Command exited with exit value: {}", exitValue);
+      } else {
+        log.info("Command started");
+      }
+
+      return outputBuilder.toString();
+    } catch (IOException e) {
+      log.error("Error executing command", e);
+      return e.getMessage();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      log.error("Command execution interrupted", e);
+      return e.getMessage();
+    }
+  }
+
+  private static void readStream(InputStream inputStream, StringBuilder outputBuilder) {
+    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+      String line;
+      while ((line = reader.readLine()) != null) {
+        outputBuilder.append(line).append(System.lineSeparator());
+      }
+    } catch (IOException e) {
+      log.error("Error reading process output", e);
+    }
+  }
+
+  /**
+   * Get the current battery level of the computer this MRL instance is running
+   * on.
+   *
+   * @return The battery level as a double from 0.0 to 100.0, expressed as a
+   *         percentage.
+   */
+  public static Double getBatteryLevel() {
+    Platform platform = Platform.getLocalInstance();
+    Double r = 100.0;
+    try {
+      if (platform.isWindows()) {
+        // String ret = Runtime.execute("cmd.exe", "/C", "WMIC.exe", "PATH",
+        // "Win32_Battery", "Get", "EstimatedChargeRemaining");
+        String ret = Runtime.execute("WMIC.exe", "PATH", "Win32_Battery", "Get", "EstimatedChargeRemaining");
+        int pos0 = ret.indexOf("\n");
+        if (pos0 != -1) {
+          pos0 = pos0 + 1;
+          int pos1 = ret.indexOf("\n", pos0);
+          String dble = ret.substring(pos0, pos1).trim();
+          try {
+            r = Double.parseDouble(dble);
+          } catch (Exception e) {
+            log.error("no Battery detected by system");
+          }
+
+          return r;
+        }
+
+      } else if (platform.isLinux()) {
+        // TODO This is incorrect, will not work when unplugged
+        // and acpitool output is different than expected,
+        // at least on Ubuntu 22.04 - consider oshi library
+        if (FileIO.isExecutableAvailable("acpi")) {
+          String ret = Runtime.execute("acpi");
+          int pos0 = ret.indexOf("%");
+
+          if (pos0 != -1) {
+            int pos1 = ret.lastIndexOf(" ", pos0);
+            // int pos1 = ret.indexOf("%", pos0);
+            String dble = ret.substring(pos1, pos0).trim();
+            try {
+              r = Double.parseDouble(dble);
+            } catch (Exception e) {
+              log.error("no Battery detected by system");
+            }
+            return r;
+          }
+          log.info(ret);
+        }
+      } else if (platform.isMac()) {
+        String ret = Runtime.execute("pmset -g batt");
+        int pos0 = ret.indexOf("Battery-0");
+        if (pos0 != -1) {
+          pos0 = pos0 + 10;
+          int pos1 = ret.indexOf("%", pos0);
+          String dble = ret.substring(pos0, pos1).trim();
+          try {
+            r = Double.parseDouble(dble);
+          } catch (Exception e) {
+            log.error("no Battery detected by system");
+          }
+          return r;
+        }
+        log.info(ret);
+      }
+
+    } catch (Exception e) {
+      log.info("execToString threw", e);
+    }
+    return r;
+  }
+
+  /**
+   * Get the local service data instance.
+   * 
+   * @return The local service data
+   * @see ServiceData#getLocalInstance()
+   */
+  public ServiceData getServiceData() {
+    return serviceData;
+  }
+
+  /**
+   * Return supported system languages
+   *
+   * @return map of languages to locales
+   */
+  public Map<String, Locale> getLanguages() {
+    return Locale.getAvailableLanguages();
+  }
+
+  /**
+   * Get a map between locale IDs and the associated {@link Locale} instance.
+   *
+   * @return A map between IDs and instances.
+   */
+  @Override
+  public Map<String, Locale> getLocales() {
+    return locales;
+  }
+
+  /**
+   * Set the locales by passing a list of locale IDs.
+   *
+   * @param codes
+   *          A list of locale IDs
+   * @return A map between the IDs and the Locale instances.
+   */
+  public Map<String, Locale> setLocales(String... codes) {
+    locales = Locale.getLocaleMap(codes);
+    return locales;
+  }
+
+  /**
+   * @return get the Security singleton
+   *
+   *
+   */
+  static public Security getSecurity() {
+    return Security.getInstance();
+  }
+
+  /**
+   * Execute a program with arguments, if any. Wraps
+   * {@link java.lang.Runtime#exec(String[])}.
+   *
+   * @param cmd
+   *          A list with the program name as the first element and any
+   *          arguments as the subsequent elements.
+   * @return The Process spawned by the execution
+   * @throws IOException
+   *           if an I/O error occurs while spawning the process
+   */
+  public static Process exec(String... cmd) throws IOException {
+    // FIXME - can't return a process - it will explode in serialization
+    // but we might want to keep it and put it on a transient map
+    log.info("Runtime exec {}", Arrays.toString(cmd));
+    Process p = java.lang.Runtime.getRuntime().exec(cmd);
+    return p;
+  }
+
+  /**
+   * Get all the options passed on the command line when MyRobotLab is executed.
+   *
+   * @return The options that were passed on the command line
+   */
+  public static CmdOptions getOptions() {
+    return options;
+  }
+
+  /**
+   * TODO Unimplemented
+   * 
+   * @param sd
+   *          ServiceData to use
+   * @return sd
+   */
+  public ServiceData setServiceTypes(ServiceData sd) {
+    return sd;
+  }
+
+  /**
+   * FIXME - describe will have the capability to describe many aspects of a
+   * running service. Default behavior will show a list of local names, but
+   * depending on input criteria it should be possible to show * interfaces *
+   * service data * service methods * details of a service method * help/javadoc
+   * of a service method * list of other known instances * levels of detail, or
+   * lists of fields to display * meaningful default
+   *
+   * FIXME - input parameters will need to change - at some point, a subscribe
+   * to describe, and appropriate input parameters should replace the current
+   * onRegistered system
+   *
+   * @param type
+   *          t
+   * @param id
+   *          i
+   * @param remoteUuid
+   *          remote id
+   * @return describe results
+   *
+   */
+  public DescribeResults describe(String type, String id, String remoteUuid) {
+    DescribeQuery query = new DescribeQuery(type, remoteUuid);
+    return describe(type, query);
+  }
+
+  /**
+   * Get a default DescribeResults from this instance.
+   *
+   * @return A default description of this instance
+   */
+  public DescribeResults describe() {
+    // default query
+    return describe("platform", null);
+  }
+
+  /**
+   * Describe results returns the information of a "describe" which can be
+   * detailed information regarding services, theire methods and input or output
+   * types.
+   * <p>
+   * FIXME - describe(String[] filters) where filter can be name, type, local,
+   * state, etc
+   * <p>
+   * FIXME uuid and query are unused
+   *
+   * @param uuid
+   *          u
+   * @param query
+   *          q
+   * @return describe results
+   *
+   *
+   *
+   */
+  public DescribeResults describe(String uuid, DescribeQuery query) {
+
+    DescribeResults results = new DescribeResults();
+    results.setStatus(Status.success("Ahoy!"));
+
+    String fullname = null;
+
+    try {
+
+      results.setId(getId());
+      results.setPlatform(Platform.getLocalInstance());
+
+      // broadcast completed connection information
+      invoke("getConnections"); // FIXME - why isn't this done before ???
+
+      Set<String> set = registry.keySet();
+      String[] list = new String[set.size()];
+      set.toArray(list);
+
+      // TODO - filtering on what is broadcasted or re-broadcasted
+      for (int i = 0; i < list.length; ++i) {
+        fullname = list[i];
+        ServiceInterface si = registry.get(fullname);
+
+        Registration registration = new Registration(si);
+
+        results.addRegistration(registration);
+      }
+
+    } catch (Exception e) {
+      log.error("describe threw on {}", fullname, e);
+    }
+
+    return results;
+  }
+
+  /**
+   * Describe results from remote query to describe
+   *
+   * @param results
+   *          describe results
+   *
+   *
+   */
+  public void onDescribe(DescribeResults results) {
+    List<Registration> reservations = results.getReservations();
+    if (reservations != null) {
+      for (Registration reservation : reservations) {
+        if ("runtime".equals(reservation.getName()) && !getId().equals(reservation.getId())) {
+          // If there's a reservation for a remote runtime, subscribe to its
+          // registered
+          // Maybe this should be done in register()?
+          subscribe(reservation.getFullName(), "registered");
+        }
+        register(reservation);
+      }
+    }
+
+  }
+
+  /**
+   * IMPORTANT IMPORTANT IMPORTANT - Newly connected remote mrl processes blas a
+   * list of registrations through onRegistered messages, for each service they
+   * currently have in their registry. This process will send a list of
+   * registrations to the newly connected remote process. If the "registered"
+   * event is subscribed, any newly created service will be broadcasted thorough
+   * this publishing point as well.
+   *
+   * TODO - write filtering, configuration, or security which affects what can
+   * be registered
+   *
+   * Primarily, this is where new services are registered from remote systems
+   *
+   *
+   */
+  public void onRegistered(Registration registration) {
+    try {
+      // check if registered ?
+
+      // TODO - filtering - include/exclude
+
+      String fullname = registration.getName() + "@" + registration.getId();
+      if (!registry.containsKey(fullname)) {
+        register(registration);
+        if (fullname.startsWith("runtime@")) {
+          // We want to TELL remote runtime if we have new registrations - we'll
+          // send them
+          // to it's runtime
+          // subscribe(fullname, "registered");
+          // subscribe(fullname, "released");
+          // IMPORTANT w
+          addListener("registered", fullname);
+          addListener("released", fullname);
+        }
+      } else {
+        log.info("{} onRegistered already registered", fullname);
+      }
+    } catch (Exception e) {
+      log.error("onRegistered threw {}", registration, e);
+    }
+  }
+
+  /**
+   * Listener for authentication.
+   * 
+   * @param response
+   *          The results from a foreign instance's
+   *          {@link Runtime#describe(String, DescribeQuery)}
+   */
+  public void onAuthenticate(DescribeResults response) {
+    log.info("onAuthenticate {}", response);
+  }
+
+  /**
+   * Get a list of metadata about all services local to this instance.
+   * 
+   * @return A list of metadata about local services
+   * @see ServiceData#getServiceTypes()
+   */
+  public List<MetaData> getServiceTypes() {
+    List<MetaData> filteredTypes = new ArrayList<>();
+    for (MetaData metaData : serviceData.getServiceTypes()) {
+      if (metaData.isAvailable()) {
+        filteredTypes.add(metaData);
+      }
+    }
+    return filteredTypes;
+  }
+
+  /**
+   * Register a connection route from one instance to this one.
+   *
+   * @param uuid
+   *          Unique ID for a connecting client
+   * @param id
+   *          Name or ID of the connecting client
+   * @param connection
+   *          Details of the connection
+   */
+  @Override
+  public void addConnection(String uuid, String id, Connection connection) {
+    Connection attr = null;
+    if (!connections.containsKey(uuid)) {
+      attr = connection;
+      invoke("publishConnect", connection);
+    } else {
+      attr = connections.get(uuid);
+      attr.putAll(connection);
+    }
+    connections.put(uuid, attr);
+    // String id = (String)attr.get("id");
+
+    addRoute(id, uuid, 10);
+  }
+
+  /**
+   * Unregister all connections that a specified client has made.
+   *
+   * @param uuid
+   *          The ID of the client
+   */
+  @Override
+  public void removeConnection(String uuid) {
+
+    Connection conn = connections.remove(uuid);
+
+    if (conn != null) {
+      invoke("publishDisconnect", uuid);
+      invoke("getConnections");
+
+      Set<String> remoteIds = routeTable.getAllIdsFor(uuid);
+      for (String id : remoteIds) {
+        unregisterId(id);
+      }
+      routeTable.removeRoute(uuid);
+    }
+  }
+
+  /**
+   * Unregister all services originating from the instance with the given ID.
+   *
+   * @param id
+   *          The ID of the instance that is being unregistered
+   */
+  public void unregisterId(String id) {
+    Set<String> names = new HashSet<>(registry.keySet());
+    for (String name : names) {
+      if (name.endsWith("@" + id)) {
+        unregister(name);
+      }
+    }
+  }
+
+  public String publishDisconnect(String uuid) {
+    return uuid;
+  }
+
+  // FIXME - filter only serializable objects ?
+  public Connection publishConnect(Connection attributes) {
+    return attributes;
+  }
+
+  /**
+   * globally get all client
+   *
+   * @return connection map
+   */
+  public Map<String, Connection> getConnections() {
+    return connections;
+  }
+
+  /**
+   * separated by connection - send connection name and get filter results back
+   * for a specific connections connected clients
+   *
+   * @param gatwayName
+   *          name
+   * @return map of connections
+   *
+   */
+  public Map<String, Connection> getConnections(String gatwayName) {
+    Map<String, Connection> ret = new HashMap<>();
+    for (String uuid : connections.keySet()) {
+      Connection c = connections.get(uuid);
+      String gateway = (String) c.get("gateway");
+      if (gatwayName == null || gateway.equals(gatwayName)) {
+        ret.put(uuid, c);
+      }
+    }
+    return ret;
+  }
+
+  /**
+   * @return list connections - current connection names to this mrl runtime
+   *
+   */
+  public Map<String, Connection> lc() {
+    return getConnections();
+  }
+
+  /**
+   * get a specific clients data
+   *
+   * @param uuid
+   *          uuid to get
+   * @return connection for uuid
+   *
+   */
+  public Connection getConnection(String uuid) {
+    return connections.get(uuid);
+  }
+
+  /**
+   * @return Globally get all connection uuids
+   *
+   */
+  public List<String> getConnectionUuids() {
+    return getConnectionUuids(null);
+  }
+
+  /**
+   * Get whether a connection to the given client exists.
+   *
+   * @param uuid
+   *          Unique ID of the client to check for
+   * @return Whether a connection between this instance and the given client
+   *         exists
+   */
+  boolean connectionExists(String uuid) {
+    return connections.containsKey(uuid);
+  }
+
+  /**
+   * Get connection ids that belong to a specific gateway
+   *
+   * @param name
+   *          n
+   * @return list of uuids
+   *
+   */
+  public List<String> getConnectionUuids(String name) {
+    List<String> ret = new ArrayList<>();
+    for (String uuid : connections.keySet()) {
+      Connection c = connections.get(uuid);
+      String gateway = (String) c.get("gateway");
+      if (name == null || gateway.equals(name)) {
+        ret.add(uuid);
+      }
+    }
+    return ret;
+  }
+
+  /**
+   * Get the Class instance for a specific service.
+   *
+   * @param inName
+   *          The name of the service
+   * @return The Class of the service.
+   * @see #getFullName(String)
+   */
+  public static Class<?> getClass(String inName) {
+    String name = getFullName(inName);
+    ServiceInterface si = registry.get(name);
+    if (si == null) {
+      return null;
+    }
+    return si.getClass();
+  }
+
+  /**
+   * takes an id returns a connection uuid
+   *
+   * @param id
+   *          id
+   * @return the connection
+   *
+   */
+  public Connection getRoute(String id) {
+    return connections.get(routeTable.getRoute(id));
+  }
+
+  public RouteTable getRouteTable() {
+    return routeTable;
+  }
+
+  /**
+   * get gateway based on remote address of a msg e.g. msg.getRemoteId()
+   *
+   * @param remoteId
+   *          remote
+   * @return the gateway
+   *
+   */
+  public Gateway getGatway(String remoteId) {
+    // get a connection from the route
+    Connection conn = getRoute(remoteId);
+    if (conn == null) {
+      log.debug("no connection for id {}", remoteId);
+      return null;
+    }
+    // find the gateway managing the connection
+    return (Gateway) getService((String) conn.get("gateway"));
+  }
+
+  /**
+   * Get the full name of the service. A full name is defined as a "short name"
+   * plus the ID of the Runtime instance it is attached to. The two components
+   * are separated by an '@' character. If the given name is already a full
+   * name, it is returned immediately, otherwise a full name is constructed by
+   * assuming the service is local to this instance. Example:
+   * 
+   * <pre>
+   * {
+   *   &#64;code
+   *   String shortName = "python";
+   *
+   *   // Assume the local name is "bombastic-cherry"
+   *   String fullName = getFullName(shortName);
+   *   // fullName is now "python@bombastic-cherry"
+   *
+   *   fullName = getFullName(fullName);
+   *   // fullName is unchanged because it was already a full name
+   *
+   * }
+   * </pre>
+   *
+   *
+   * @param shortname
+   *          The name to convert to a full name
+   * @return shortname if it is already a full name, or a newly constructed full
+   *         name
+   */
+  static public String getFullName(String shortname) {
+    if (shortname == null || shortname.contains("@")) {
+      // already long form
+      return shortname;
+    }
+    // if nothing is supplied assume local
+    return String.format("%s@%s", shortname, Runtime.getInstance().getId());
+  }
+
+  @Override
+  public List<String> getClientIds() {
+    return getConnectionUuids(getName());
+  }
+
+  @Override
+  public Map<String, Connection> getClients() {
+    return getConnections(getName());
+  }
+
+  public void pollHosts() {
+    runtime.addTask(20000, "getHosts");
+  }
+
+  // FIXME - remove if not using ...
+  @Override
+  public void sendRemote(Message msg) throws IOException {
+    if (isLocal(msg)) {
+      log.error("msg NOT REMOTE yet sendRemote is called {}", msg);
+      return;
+    }
+
+    // get a connection from the route
+    Connection conn = getRoute(msg.getId());
+    if (conn == null) {
+      log.error("could not get connection for {} from msg {}", msg.getId(), msg);
+      return;
+    }
+
+    // two possible types of "remote" for this gateway cli & ws
+    if ("Cli".equals(conn.get("c-type"))) {
+      invoke("publishCli", msg);
+
+      InProcessCli cli = ((InProcessCli) conn.get("cli"));
+      cli.onMsg(msg);
+
+    } else {
+      // websocket Client !
+      WsClient client = (WsClient) conn.get("c-client");
+      if (client == null) {
+        log.error("could not get client for connection {}", msg.getId());
+        return;
+      }
+
+      /**
+       * ======================================================================
+       * DYNAMIC ROUTE TABLE - outbound msg hop starts now
+       */
+
+      // add our id - we don't want to see it again
+      msg.addHop(getId());
+
+      log.info("<== {}.{} <-- {}", msg.name, msg.method, msg.sender);
+
+      /**
+       * ======================================================================
+       */
+
+      client.send(CodecUtils.toJsonMsg(msg));
+    }
+  }
+
+  public Object publishCli(Message msg) {
+    if (msg.data == null || msg.data.length == 0) {
+      return null;
+    }
+    return msg.data[0];
+  }
+
+  /**
+   * DONT MODIFY NAME - JUST work on is Local - and InvokeOn should handle it
+   *
+   * if the incoming Message's remote Id is the (same as ours) OR (it can't be
+   * found it our route table) - peel it off and treat it as local.
+   *
+   * if we have an @{id/connection} but do not have the connection - we'll peel
+   * off the @{id/connection} and treat it as local if id is ours - peel it off
+   * !
+   */
+  @Override
+  public boolean isLocal(Message msg) {
+
+    if (msg.getId() == null || getId().equals(msg.getId())) {
+      return true;
+    }
+
+    return false;
+  }
+
+  public Object localizeDefault(String key) {
+    key = key.toUpperCase();
+    return defaultLocalization.get(key);
+  }
+
+  static public void setAllLocales(String code) {
+    for (ServiceInterface si : getLocalServices().values()) {
+      si.setLocale(code);
+    }
+  }
+
+  @Override
+  public String created(String name) {
+    return name;
+  }
+
+  @Override
+  public String started(String name) {
+    // if this is to be used as a callback in Python
+    // users typically would want simple name ... not "fullname"
+
+    return name;
+  }
+
+  @Override
+  public String stopped(String name) {
+    return name;
+  }
+
+  /**
+   * Wrapper for {@link ServiceData#getMetaData(String, String)}
+   * 
+   * @param serviceName
+   *          The name of the service
+   * @param serviceType
+   *          The type of the service
+   * @return The metadata of the service.
+   */
+  public static MetaData getMetaData(String serviceName, String serviceType) {
+    return ServiceData.getMetaData(serviceName, serviceType);
+  }
+
+  /**
+   * Wrapper for {@link ServiceData#getMetaData(String)}
+   * 
+   * @param serviceType
+   *          The type of the service
+   * @return The metadata of the service.
+   */
+  public static MetaData getMetaData(String serviceType) {
+    return ServiceData.getMetaData(serviceType);
+  }
+
+  /**
+   * Whether the singleton has been created
+   * 
+   * @return Whether the singleton exists
+   */
+  public static boolean exists() {
+    return runtime != null;
+  }
+
+  /**
+   * Attempt to get the most likely valid address priority would be a lan
+   * address - possibly the smallest class
+   *
+   * @return string address
+   *
+   */
+  public String getAddress() {
+    List<String> addresses = getIpAddresses();
+    if (addresses.size() > 0) {
+
+      // class priority
+      for (String ip : addresses) {
+        if (ip.startsWith("192.168")) {
+          return ip;
+        }
+      }
+
+      for (String ip : addresses) {
+        if (ip.startsWith("172.")) {
+          return ip;
+        }
+      }
+
+      for (String ip : addresses) {
+        if (ip.startsWith("10.")) {
+          return ip;
+        }
+      }
+
+      // give up - return first :P
+      return addresses.get(0);
+    }
+    return null;
+  }
+
+  public List<Host> getHosts() {
+    List<String> ips = getIpAddresses();
+    String selectedIp = (ips.size() == 1) ? ips.get(0) : null;
+    if (selectedIp == null) {
+      for (String ip : ips) {
+        if ((selectedIp != null) && (ip.startsWith(("192.")))) {
+          selectedIp = ip;
+        } else if (selectedIp == null) {
+          selectedIp = ip;
+        }
+      }
+    }
+    String subnet = selectedIp.substring(0, selectedIp.lastIndexOf("."));
+    return getHosts(subnet);
+  }
+
+  public List<Host> getHosts(String subnet) {
+
+    if (hosts == null) {
+      hosts = new HashMap<String, Host>();
+      File check = new File(FileIO.gluePaths(getDataDir(), "hosts.json"));
+      if (check.exists()) {
+        try {
+          Host[] hf = CodecUtils.fromJson(FileIO.toString(check), Host[].class);
+          for (Host h : hf) {
+            hosts.put(h.ip, h);
+          }
+          info("found %d saved hosts", hosts.size());
+        } catch (Exception e) {
+          error("could not load %s - %s", check, e.getMessage());
+        }
+      }
+    }
+
+    int timeout = 1500;
+    try {
+      for (int i = 1; i < 255; i++) {
+        Thread pinger = new Thread(new Pinger(this, hosts, subnet + "." + i, timeout), "pinger-" + i);
+        pinger.start();
+      }
+    } catch (Exception e) {
+      log.error("getHosts threw", e);
+    }
+    List<Host> h = new ArrayList<>();
+    for (Host hst : hosts.values()) {
+      if (hst.lastActiveTs != null) {
+        h.add(hst);
+      }
+    }
+    return h;
+  }
+
+  public Host publishFoundHost(Host host) {
+    log.info("found host {}", host);
+    return host;
+  }
+
+  public Host publishFoundNewHost(Host host) {
+    log.info("found new host {}", host);
+    return host;
+  }
+
+  public Host publishLostHost(Host host) {
+    log.info("lost host {}", host);
+    return host;
+  }
+
+  public void saveHosts() throws IOException {
+    FileOutputStream fos = new FileOutputStream(FileIO.gluePaths(getDataDir(), "hosts.json"));
+    List<Host> h = new ArrayList<>(hosts.values());
+    String json = CodecUtils.toPrettyJson(h);
+    fos.write(json.getBytes());
+    fos.close();
+  }
+
+  /**
+   * start python interactively at the command line
+   */
+  public void python() {
+    if (cli == null) {
+      startInteractiveMode();
+    }
+    start("python", "Python");
+    // since we've suscribed to pythons st
+    cli.relay("python", "exec", "publishStdOut");
+    cli.relay("python", "exec", "publishStdError");
+    Logging logging = LoggingFactory.getInstance();
+    logging.removeAllAppenders();
+  }
+
+  /**
+   * Main entry point for the MyRobotLab Runtime Check CmdOptions for list of
+   * options -h help -v version -list jvm args -Dhttp.proxyHost=webproxy
+   * f-Dhttp.proxyPort=80 -Dhttps.proxyHost=webproxy -Dhttps.proxyPort=80
+   *
+   * @param args
+   *          cmd line args from agent spawn
+   *
+   */
+  public static void main(String[] args) {
+
+    try {
+
+      // loading args
+      globalArgs = args;
+      new CommandLine(options).parseArgs(args);
+      log.info("in args {}", Launcher.toString(args));
+      log.info("options {}", CodecUtils.toJson(options));
+      log.info("\n" + Launcher.banner);
+
+      // creating initial data/config directory
+      File cfgRoot = new File(ROOT_CONFIG_DIR);
+      cfgRoot.mkdirs();
+
+      // initialize logging
+      initLog();
+
+      // extract if necessary
+      FileIO.extractResources();
+
+      // help and exit
+      if (options.help) {
+        mainHelp();
+        return;
+      }
+
+      // start.yml file is required, if not pre-existing
+      // is created immediately. It contains static information
+      // which needs to be available before a Runtime is created
+      Runtime.startYml = ConfigUtils.loadStartYml();
+
+      // resolve configName before starting getting runtime configuration
+      Runtime.configName = (startYml.enable) ? startYml.config : "default";
+      if (options.config != null) {
+        // cmd line options has the highest priority
+        Runtime.configName = options.config;
+      }
+
+      // start.yml is processed, config name is set, runtime config
+      // is resolved, now we can start instance
+      Runtime.getInstance();
+
+      if (options.install != null) {
+        // resetting log level to info
+        // for an install otherwise ivy
+        // info will not be shown in the terminal
+        // during install of dependencies
+        // which makes users panic and hit ctrl+C
+        setLogLevel("info");
+
+        // we start the runtime so there is a status publisher which will
+        // display status updates from the repo install
+        log.info("requesting install");
+        Repo repo = getInstance().getRepo();
+        if (options.install.length == 0) {
+          repo.install(LIBRARIES, (String) null);
+        } else {
+          for (String service : options.install) {
+            repo.install(LIBRARIES, service);
+          }
+        }
+        shutdown();
+        return;
+      }
+
+    } catch (Exception e) {
+      log.error("runtime exception", e);
+      Runtime.mainHelp();
+      shutdown();
+      log.error("main threw", e);
+    }
+  }
+
+  public static void initLog() {
+    if (options != null) {
+      LoggingFactory.init(options.logLevel);
+    } else {
+      LoggingFactory.init("info");
+    }
+  }
+
+  public void test() {
+    for (int statusCnt = 0; statusCnt < 500; statusCnt++) {
+      statusCnt++;
+      invoke("publishStatus", Status.info("this is status %d", statusCnt));
+    }
+  }
+
+  public Connection getConnectionFromId(String remoteId) {
+    for (Connection c : connections.values()) {
+      if (c.getId().equals(remoteId)) {
+        return c;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * A gateway is responsible for creating a key to associate a unique
+   * "Connection". This key should be retrievable, when a msg arrives at the
+   * service which needs to be sent remotely. This key is used to get the
+   * "Connection" to send the msg remotely
+   *
+   * @param string
+   *          s
+   * @param uuid
+   *          u
+   *
+   */
+  public void addLocalGatewayKey(String string, String uuid) {
+    routeTable.addLocalGatewayKey(string, uuid);
+  }
+
+  public boolean containsRoute(String remoteId) {
+    return routeTable.contains(remoteId);
+  }
+
+  public String getConnectionUuidFromGatewayKey(String gatewayKey) {
+    return routeTable.getConnectionUuid(gatewayKey);
+  }
+
+  /**
+   * This helper method will create, load then start a service
+   *
+   * @param name
+   *          - name of instance
+   * @param type
+   *          - type
+   * @return returns the service in the form of a ServiceInterface
+   */
+  static public ServiceInterface loadAndStart(String name, String type) {
+    ServiceInterface s = null;
+    try {
+      s = create(name, type);
+      s.load();
+      s.startService();
+    } catch (Exception e) {
+      log.error("loadAndStart threw", e);
+    }
+    return s;
+  }
+
+  /**
+   * DEFAULT IF NOTHING EXISTS DO NOT DEFAULT SOMETHING THAT'S ALREADY IN PLAN
+   * OVERRIDE WITH FILE
+   * 
+   * Load a single service entry into the plan through yml or default. This
+   * method is responsible for resolving the Type and ServiceConfig for a single
+   * service. Since some service Types are composites and require Peers, it can
+   * potentially be recursive. The level of overrides are from highest priority
+   * to lowest :
+   * 
+   * <pre>
+   *       if a Plan definition of {name} exists, use it   - "current" plan definition !
+   *       /data/config/{configName}/{service}.yml          - user's yml override
+   *       /resource/config/{configName}/{service}.yml      - system yml default
+   *       {ServiceConfig}.java                             - system java type default
+   * 
+   * 
+   * </pre>
+   * 
+   * @param plan
+   *          - plan to load
+   * @param name
+   *          - name of service
+   * @param type
+   *          - type of service
+   * @param start
+   *          - weather to specify in RuntimeConfig.registry to "start" this
+   *          service when createFromPlan is run
+   * @param level
+   *          - level of the depth, services may load peers which in turn will
+   *          load more, this is the depth of recursion
+   * @return
+   * @throws IOException
+   */
+  public Plan loadService(Plan plan, String name, String type, boolean start, int level) throws IOException {
+    synchronized (processLock) {
+
+      if (plan == null) {
+        log.error("plan required to load a system");
+        return null;
+      }
+
+      log.info("loading - {} {} {}", name, type, level);
+      // from recursive memory definition
+      ServiceConfig sc = plan.get(name);
+
+      // HIGHEST PRIORITY - OVERRIDE WITH FILE
+      String configPath = runtime.getConfigPath();
+      String configFile = configPath + fs + name + ".yml";
+
+      // PRIORITY #1
+      // find if a current yml config file exists - highest priority
+      log.debug("priority #1 user's yml override {} ", configFile);
+      ServiceConfig fileSc = readServiceConfig(Runtime.getInstance().getConfigName(), name);
+      if (fileSc != null) {
+        // if definition exists in file form, it overrides current memory one
+        sc = fileSc;
+      } else if (sc != null) {
+        // if memory config is available but not file
+        // we save it
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
+      }
+
+      // special conflict case - type is specified, but its not the same as
+      // file version - in that case specified parameter type wins and
+      // overwrites
+      // config. User can force type by supplying one as a parameter, however,
+      // the
+      // recursive
+      // call other peer types will have name/file.yml definition precedence
+      if ((type != null && sc != null && !type.equals(sc.type) && level == 0) || (sc == null)) {
+        if (sc != null) {
+          warn("type %s overwriting type %s specified in %s.yml file", type, sc.type, name);
+        }
+        ServiceConfig.getDefault(plan, name, type);
+        sc = plan.get(name);
+
+        // create new file if it didn't exist or overwrite it if new type is
+        // required
+        String yml = CodecUtils.toYaml(sc);
+        FileIO.toFile(configFile, yml);
+      }
+
+      if (sc == null && type == null) {
+        log.error("no local config and unknown type");
+        return plan;
+      }
+
+      // finalize
+      if (sc != null) {
+        plan.put(name, sc);
+        // RECURSIVE load peers
+        Map<String, Peer> peers = sc.getPeers();
+        for (String peerKey : peers.keySet()) {
+          Peer peer = peers.get(peerKey);
+          // recursive depth load - parent and child need to be started
+          runtime.loadService(plan, peer.name, peer.type, start && peer.autoStart, level + 1);
+        }
+
+        // valid service config at this point - now determine if its supposed to
+        // start or not
+        // if its level 0 then it was requested by user or config - so it needs
+        // to
+        // start
+        // if its not level 0 then it was loaded because peers were defined and
+        // appropriate config loaded
+        // peer.autoStart should determine if the peer starts if not explicitly
+        // requested by the
+        // user or config
+        if (level == 0 || start) {
+          plan.addRegistry(name);
+        }
+
+      } else {
+        log.info("could not load {} {} {}", name, type, level);
+      }
+
+      return plan;
+    }
+  }
+
+  /**
+   * read a service's configuration, in the context of current config set name
+   * or default
+   * 
+   * @param name
+   * @return
+   */
+  public ServiceConfig readServiceConfig(String name) {
+    return readServiceConfig(name, new StaticType<>() {
+    });
+  }
+
+  /**
+   * read a service's configuration, in the context of current config set name
+   * or default
+   * 
+   * @param name
+   * @return
+   */
+  public <C extends ServiceConfig> C readServiceConfig(String name, StaticType<C> configType) {
+    return readServiceConfig(null, name, configType);
+  }
+
+  public ServiceConfig readServiceConfig(String configName, String name) {
+    return readServiceConfig(configName, name, new StaticType<>() {
+    });
+  }
+
+  /**
+   *
+   * @param configName
+   *          - filename or dir of config set
+   * @param name
+   *          - name of config file within that dir e.g. {name}.yml
+   * @return
+   */
+  public <C extends ServiceConfig> C readServiceConfig(String configName, String name, StaticType<C> configType) {
+    // if config path set and yaml file exists - it takes precedence
+
+    if (configName == null) {
+      configName = runtime.getConfigName();
+    }
+
+    if (configName == null) {
+      log.info("config name is null cannot load {} file system", name);
+      return null;
+    }
+
+    String filename = ROOT_CONFIG_DIR + fs + configName + fs + name + ".yml";
+    File check = new File(filename);
+    C sc = null;
+    if (check.exists()) {
+      try {
+        sc = CodecUtils.readServiceConfig(filename, configType);
+      } catch (ConstructorException e) {
+        error("config %s invalid %s %s. Please remove it from the file.", name, filename, e.getCause().getMessage());
+      } catch (Exception e) {
+        error("config could not load %s file is invalid", filename);
+      }
+    }
+    return sc;
+  }
+
+  public String publishConfigLoaded(String name) {
+    return name;
+  }
+
+  @Override
+  public RuntimeConfig apply(RuntimeConfig config) {
+    super.apply(config);
+
+    setLocale(config.locale);
+
+    if (config.id == null) {
+      config.id = NameGenerator.getName();
+    }
+
+    if (config.logLevel != null) {
+      setLogLevel(config.logLevel);
+    }
+
+    if (config.virtual != null) {
+      info("setting virtual to %b", config.virtual);
+      setAllVirtual(config.virtual);
+    }
+
+    // APPLYING A RUNTIME CONFIG DOES NOT PROCESS THE REGISTRY
+    // USE startConfig(name)
+
+    broadcastState();
+    return config;
+  }
+
+  /**
+   * release the current config
+   */
+  static public void releaseConfig() {
+    String currentConfigPath = Runtime.getInstance().getConfigName();
+    if (currentConfigPath != null) {
+      releaseConfigPath(currentConfigPath);
+    }
+  }
+
+  /**
+   * wrapper
+   * 
+   * @param configName
+   */
+  static public void releaseConfig(String configName) {
+    setConfig(configName);
+    releaseConfigPath(Runtime.getInstance().getConfigName());
+  }
+
+  /**
+   * Release a configuration set - this depends on a runtime file - and it will
+   * release all the services defined in it, with the exception of the
+   * originally started services
+   * 
+   * @param configPath
+   *          config set to release
+   *
+   */
+  static public void releaseConfigPath(String configPath) {
+    try {
+      String filename = ROOT_CONFIG_DIR + fs + Runtime.getInstance().getConfigName() + fs + "runtime.yml";
+      String releaseData = FileIO.toString(new File(filename));
+      RuntimeConfig config = CodecUtils.fromYaml(releaseData, RuntimeConfig.class);
+      List<String> registry = config.getRegistry();
+      Collections.reverse(Arrays.asList(registry));
+
+      // get starting services if any entered on the command line
+      // -s log Log webgui WebGui ... etc - these will be protected
+      List<String> startingServices = new ArrayList<>();
+      if (options.services.size() % 2 == 0) {
+        for (int i = 0; i < options.services.size(); i += 2) {
+          startingServices.add(options.services.get(i));
+        }
+      }
+
+      for (String name : registry) {
+        if (startingServices.contains(name)) {
+          continue;
+        }
+        release(name);
+      }
+    } catch (Exception e) {
+      Runtime.getInstance().error("could not release %s", configPath);
+    }
+  }
+
+  public static String getConfigRoot() {
+    return ROOT_CONFIG_DIR;
+  }
+
+  /**
+   * wrapper for saveConfigPath with default prefix path supplied
+   * 
+   * @param configName
+   * @return
+   */
+  static public boolean saveConfig(String configName) {
+    Runtime runtime = Runtime.getInstance();
+    if (configName == null) {
+      runtime.error("saveConfig require a name cannot be null");
+      return false;
+    }
+    boolean ret = runtime.saveService(configName, null, null);
+    runtime.broadcastState();
+    return ret;
+  }
+
+  /**
+   * 
+   * Saves the current runtime, all services and all configuration for each
+   * service in the current "config path", if the config path does not exist
+   * will error
+   *
+   * @param configName
+   *          - config set name if null defaults to default
+   * @param serviceName
+   *          - service name if null defaults to saveAll
+   * @param filename
+   *          - if not explicitly set - will be standard yml filename
+   * @return - true if all goes well
+   */
+  public boolean saveService(String configName, String serviceName, String filename) {
+    try {
+
+      if (configName == null) {
+        error("config name cannot be null");
+        return false;
+      }
+
+      setConfig(configName);
+
+      String configPath = ROOT_CONFIG_DIR + fs + configName;
+
+      // save running services
+      Set<String> servicesToSave = new HashSet<>();
+
+      // conditional boolean to flip and save a config name to start.yml ?
+      if (startYml.enable) {
+        startYml.config = configName;
+        FileIO.toFile("start.yml", CodecUtils.toYaml(startYml));
+      }
+
+      if (serviceName == null) {
+        // all services
+        servicesToSave = getLocalServices().keySet();
+      } else {
+        // single service
+        servicesToSave.add(serviceName);
+      }
+
+      for (String s : servicesToSave) {
+        ServiceInterface si = getService(s);
+        // TODO - switch to save "NON FILTERED" config !!!!
+        // get filtered clone of config for saving
+        ServiceConfig config = si.getFilteredConfig();
+        String data = CodecUtils.toYaml(config);
+        String ymlFileName = configPath + fs + CodecUtils.getShortName(s) + ".yml";
+        FileIO.toFile(ymlFileName, data.getBytes());
+        info("saved %s", ymlFileName);
+      }
+
+      invoke("publishConfigList");
+      return true;
+
+    } catch (Exception e) {
+      error(e);
+    }
+    return false;
+  }
+
+  public String getConfigName() {
+    return configName;
+  }
+
+  public boolean isProcessingConfig() {
+    return processingConfig;
+  }
+
+  /**
+   * Sets the directory for the current config. This will be under configRoot +
+   * fs + configName. Static wrapper around setConfigName - so it can be used in
+   * the same way as all the other common static service methods
+   * 
+   * @param name
+   *          - config dir name under data/config/{config}
+   * @return config dir name
+   */
+  public static String setConfig(String name) {
+    if (name == null) {
+      log.error("config cannot be null");
+      if (runtime != null) {
+        runtime.error("config cannot be null");
+      }
+      return null;
+    }
+
+    if (name.contains(fs)) {
+      log.error("invalid character " + fs + " in configuration name");
+      if (runtime != null) {
+        runtime.error("invalid character " + fs + " in configuration name");
+      }
+      return name;
+    }
+
+    configName = name.trim();
+
+    File configDir = new File(ROOT_CONFIG_DIR + fs + name);
+    if (!configDir.exists()) {
+      configDir.mkdirs();
+    }
+
+    if (runtime != null) {
+      runtime.invoke("publishConfigList");
+      runtime.invoke("getConfigName");
+    }
+
+    return configName;
+  }
+
+  public String deleteConfig(String configName) {
+
+    File trashDir = new File(DATA_DIR + fs + "trash");
+    if (!trashDir.exists()) {
+      trashDir.mkdirs();
+    }
+
+    File configDir = new File(ROOT_CONFIG_DIR + fs + configName);
+    // Create a new directory in the trash with a timestamp to avoid name
+    // conflicts
+    File trashTargetDir = new File(trashDir, configName + "_" + System.currentTimeMillis());
+    try {
+      // Use Files.move to move the directory atomically
+      Files.move(configDir.toPath(), trashTargetDir.toPath(), StandardCopyOption.REPLACE_EXISTING);
+      log.info("Config moved to trash: " + trashTargetDir.getAbsolutePath());
+      invoke("publishConfigList");
+    } catch (IOException e) {
+      error("Failed to move config directory to trash: " + e.getMessage());
+      return null; // Return null or throw a custom exception to indicate
+                   // failure
+    }
+
+    return configName;
+  }
+
+  // FIXME - move this to service and add default (no servicename) method
+  // signature
+  @Deprecated /*
+               * I don't think this was a good solution - to handle interface
+               * lists in the js client - the js runtime should register for
+               * lifecycle events, the individiual services within that js
+               * runtime should only have local event handling to change attach
+               * lists
+               */
+  public void registerForInterfaceChange(String requestor, Class<?> interestedInterface) {
+    registerForInterfaceChange(interestedInterface.getCanonicalName());
+  }
+
+  /**
+   * Builds the requestedAttachMatrix which is a mapping between new types and
+   * their requested interfaces - interfaces they are interested in.
+   *
+   * This data should be published whenever new "Type" definitions are found
+   *
+   * @param targetedInterface
+   *          - interface this add new interface to requested interfaces - add
+   *          current names of services which fulfill that interface "IS ASKING"
+   *
+   */
+  public void registerForInterfaceChange(String targetedInterface) {
+    // boolean changed
+    Set<String> namesForRequestedInterface = interfaceToNames.get(targetedInterface);
+    if (namesForRequestedInterface == null) {
+      namesForRequestedInterface = new HashSet<>();
+      interfaceToNames.put(targetedInterface, namesForRequestedInterface);
+    }
+
+    // search through interfaceToType to find all types that implement this
+    // interface
+
+    if (interfaceToType.containsKey(targetedInterface)) {
+      Set<String> types = interfaceToType.get(targetedInterface);
+      if (types != null) {
+        for (String type : types) {
+          Set<String> names = typeToNames.get(type);
+          namesForRequestedInterface.addAll(names);
+        }
+      }
+    }
+    invoke("publishInterfaceToNames");
+  }
+
+  /**
+   * Published whenever a new service type definition if found
+   *
+   * @return
+   */
+  public Map<String, Set<String>> publishInterfaceTypeMatrix() {
+    return interfaceToType;
+  }
+
+  public Map<String, Set<String>> publishInterfaceToNames() {
+    return interfaceToNames;
+  }
+
+  static public Plan saveDefault(String className) {
+    try {
+      Runtime runtime = Runtime.getInstance();
+      return runtime.saveDefault(className.toLowerCase(), className);
+    } catch (Exception e) {
+      log.error("saving default config failed", e);
+    }
+    return null;
+  }
+
+  /**
+   * Helper method - returns if a service is started
+   *
+   * @param name
+   *          - name of service
+   * @return - true if started
+   */
+  static public boolean isStarted(String name) {
+    String fullname = null;
+    if (name == null) {
+      return false;
+    }
+    if (!name.contains("@")) {
+      fullname = name + "@" + Runtime.getInstance().getId();
+    } else {
+      fullname = name;
+    }
+    if (registry.containsKey(fullname)) {
+      ServiceInterface si = registry.get(fullname);
+      return si.isRunning();
+    }
+
+    return false;
+  }
+
+  /**
+   * Load all configuration files from a given directory.
+   *
+   * @param configPath
+   *          The directory to load from
+   */
+  public static void loadConfigPath(String configPath) {
+
+    Runtime.setConfig(configPath);
+    Runtime runtime = Runtime.getInstance();
+
+    String configSetDir = runtime.getConfigName() + fs + runtime.getConfigName();
+    File check = new File(configSetDir);
+    if (configPath == null || configPath.isEmpty() || !check.exists() || !check.isDirectory()) {
+      runtime.error("config set %s does not exist or is not a directory", check.getAbsolutePath());
+      return;
+    }
+
+    File[] configFiles = check.listFiles();
+    runtime.info("%d config files found", configFiles.length);
+    for (File f : configFiles) {
+      if (!f.getName().toLowerCase().endsWith(".yml")) {
+        log.info("{} - none yml file found in config set", f.getAbsolutePath());
+      } else {
+        runtime.loadFile(f.getAbsolutePath());
+      }
+    }
+  }
+
+  /**
+   * Load a service from a file
+   * 
+   * @param path
+   *          The full path of the file to load - this DOES NOT set the
+   *          configPath
+   */
+  public void loadFile(String path) {
+    try {
+      File f = new File(path);
+      if (!f.exists() || f.isDirectory()) {
+        error("loadFile cannot load %s - it does not exist", path);
+        return;
+      }
+      String name = f.getName().substring(0, f.getName().length() - 4);
+      ServiceConfig sc = CodecUtils.readServiceConfig(path);
+      loadService(new Plan("runtime"), name, sc.type, true, 0);
+    } catch (Exception e) {
+      error("loadFile requirese");
+    }
+  }
+
+  final public Plan getDefault(String name, String type) {
+    return ServiceConfig.getDefault(new Plan("runtime"), name, type);
+  }
+
+  final public Plan saveDefault(String name, String type) {
+    return saveDefault(name, name, type, false);
+  }
+
+  final public Plan saveDefault(String name, String type, boolean fullPlan) {
+    return saveDefault(name, name, type, fullPlan);
+  }
+
+  final public Plan saveDefault(String configName, String name, String type, boolean fullPlan) {
+
+    Plan plan = ServiceConfig.getDefault(new Plan(name), name, type);
+    String configPath = ROOT_CONFIG_DIR + fs + configName;
+
+    if (!fullPlan) {
+      try {
+        String filename = configPath + fs + name + ".yml";
+        ServiceConfig sc = plan.get(name);
+        String yaml = CodecUtils.toYaml(sc);
+        FileIO.toFile(filename, yaml);
+        info("saved %s", filename);
+      } catch (IOException e) {
+        error(e);
+      }
+    } else {
+      for (String service : plan.keySet()) {
+        try {
+          String filename = configPath + fs + service + ".yml";
+          ServiceConfig sc = plan.get(service);
+          String yaml = CodecUtils.toYaml(sc);
+          FileIO.toFile(filename, yaml);
+          info("saved %s", filename);
+        } catch (IOException e) {
+          error(e);
+        }
+      }
+    }
+    return plan;
+  }
+
+  public void savePlan(String name, String type) {
+    saveDefault(name, type, true);
+  }
+
+  public void saveAllDefaults() {
+    saveAllDefaults(new File(getResourceDir()).getParent(), false);
+  }
+
+  public void saveAllDefaults(String configPath, boolean fullPlan) {
+    List<MetaData> types = serviceData.getAvailableServiceTypes();
+    for (MetaData meta : types) {
+      saveDefault(configPath + fs + meta.getSimpleName(), meta.getSimpleName().toLowerCase(), meta.getSimpleName(), fullPlan);
+    }
+  }
+
+  /**
+   * Get current runtime's config path
+   * 
+   * @return
+   */
+  public String getConfigPath() {
+    return ROOT_CONFIG_DIR + fs + configName;
+  }
+
+  /**
+   * Gets a {serviceName}.yml file config from configName directory
+   * 
+   * @param configName
+   * @param serviceName
+   * @return ServiceConfig
+   */
+  public ServiceConfig getConfig(String configName, String serviceName) {
+    return readServiceConfig(configName, serviceName);
+  }
+
+  /**
+   * Get a {serviceName}.yml file in the current config directory
+   * 
+   * @param serviceName
+   * @return
+   */
+  public ServiceConfig getConfig(String serviceName) {
+    return readServiceConfig(serviceName);
+  }
+
+  /**
+   * Save a config with a new Config
+   * 
+   * @param name
+   * @param serviceConfig
+   * @throws IOException
+   */
+  public static void saveConfig(String name, ServiceConfig serviceConfig) throws IOException {
+    String file = Runtime.ROOT_CONFIG_DIR + fs + runtime.getConfigName() + fs + name + ".yml";
+    FileIO.toFile(file, CodecUtils.toYaml(serviceConfig));
+  }
+
+  /**
+   * get the service's peer config
+   * 
+   * @param serviceName
+   * @param peerKey
+   * @return
+   */
+  public ServiceConfig getPeerConfig(String serviceName, String peerKey) {
+    ServiceConfig sc = runtime.getConfig(serviceName);
+    if (sc == null) {
+      return null;
+    }
+    Peer peer = sc.getPeer(peerKey);
+    return runtime.getConfig(peer.name);
+  }
+
+  /**
+   * Switches a service's .yml type definition while replacing the set of
+   * listeners to preserver subscriptions. Useful when switching services that
+   * support the same interface like SpeechSynthesis services etc.
+   * 
+   * @param serviceName
+   * @param type
+   * @return
+   */
+  public boolean changeType(String serviceName, String type) {
+    try {
+      ServiceConfig sc = getConfig(serviceName);
+      if (sc == null) {
+        error("could not find %s config", serviceName);
+        return false;
+      }
+      // get target
+      Plan targetPlan = getDefault(serviceName, type);
+      if (targetPlan == null || targetPlan.get(serviceName) == null) {
+        error("%s null", type);
+        return false;
+      }
+      ServiceConfig target = targetPlan.get(serviceName);
+      // replacing listeners
+      target.listeners = sc.listeners;
+      saveConfig(serviceName, target);
+      return true;
+    } catch (Exception e) {
+      error("could not save %s of type %s", serviceName, type);
+      return false;
+    }
+  }
+
+  /**
+   * Get a peer's config
+   * 
+   * @param sericeName
+   * @param peerKey
+   * @return
+   */
+  public ServiceConfig getPeer(String sericeName, String peerKey) {
+    ServiceConfig sc = getConfig(sericeName);
+    if (sc == null) {
+      return null;
+    }
+    Peer peer = sc.getPeer(peerKey);
+    if (peer == null) {
+      return null;
+    }
+    return getConfig(peer.name);
+  }
+
+  /**
+   * Removes a config set and all its files
+   * 
+   * @param configName
+   *          - name of config
+   */
+  public static void removeConfig(String configName) {
+    try {
+      log.info("removing config");
+
+      File check = new File(ROOT_CONFIG_DIR + fs + configName);
+
+      if (check.exists()) {
+        Path pathToBeDeleted = Paths.get(check.getAbsolutePath());
+        Files.walk(pathToBeDeleted).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+      }
+    } catch (Exception e) {
+      log.error("removeConfig threw", e);
+    }
+  }
 
 }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index bf611a1fd2..cebe3ea063 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -154,6 +154,8 @@ public class InMoov2Config extends ServiceConfig {
 
   public boolean virtual = false;
 
+  public boolean execScript = true;
+
   public InMoov2Config() {
   }
 
diff --git a/src/main/resources/resource/WebGui/app/views/tabsViewCtrl.js b/src/main/resources/resource/WebGui/app/views/tabsViewCtrl.js
index 6dbdf20fbc..c4e6f5edb7 100644
--- a/src/main/resources/resource/WebGui/app/views/tabsViewCtrl.js
+++ b/src/main/resources/resource/WebGui/app/views/tabsViewCtrl.js
@@ -1,5 +1,13 @@
-angular.module('mrlapp.mrl').controller('tabsViewCtrl', ['$location', '$scope', '$filter', '$timeout', 'mrl', '$state', '$stateParams', function($location, $scope, $filter, $timeout, mrl, $state, $stateParams) {
-    console.info('tabsViewCtrl $scope.$id - ' + $scope.$id)
+angular.module("mrlapp.mrl").controller("tabsViewCtrl", [
+  "$location",
+  "$scope",
+  "$filter",
+  "$timeout",
+  "mrl",
+  "$state",
+  "$stateParams",
+  function ($location, $scope, $filter, $timeout, mrl, $state, $stateParams) {
+    console.info("tabsViewCtrl $scope.$id - " + $scope.$id)
     _self = this
 
     $scope.history = []
@@ -10,65 +18,71 @@ angular.module('mrlapp.mrl').controller('tabsViewCtrl', ['$location', '$scope',
 
     // setting callback method in service so other controllers
     // can set searchText
-    $scope.setSearchText = function(text) {
-        $scope.searchText.displayName = text
+    $scope.setSearchText = function (text) {
+      $scope.searchText.displayName = text
     }
 
-    $scope.noworky = function() {
-        noWorkySvc.openNoWorkyModal($scope.panel.name)
+    $scope.noworky = function () {
+      noWorkySvc.openNoWorkyModal($scope.panel.name)
     }
 
-    $scope.updateServiceData = function() {
-        //get an updated / fresh servicedata & convert it to json
-        var servicedata = mrl.getService($scope.view_tab)
-        $scope.servicedatajson = JSON.stringify(servicedata, null, 2)
+    $scope.updateServiceData = function () {
+      //get an updated / fresh servicedata & convert it to json
+      var servicedata = mrl.getService($scope.view_tab)
+      $scope.servicedatajson = JSON.stringify(servicedata, null, 2)
     }
 
-    $scope.getName = function(panel) {
-        return panel.name
+    $scope.getName = function (panel) {
+      return panel.name
     }
 
     //service-panels & update-routine
-    var panelsUpdated = function(panels) {
-        console.debug('tabsViewCtrl.panelsUpdated ' + panels.length)
-        $scope.panels = panels
-
-        if (!$scope.view_tab && panels.length > 0 && $scope.panels[$scope.panels.length - 1].name.startsWith('intro')) {//  $scope.changeTab($scope.panels[0].name)
-        }
-
-        // if /#/service/{servicename} - change the tab
-        if ($scope.servicename) {//   $scope.changeTab($scope.servicename)
-        }
+    var panelsUpdated = function (panels) {
+      console.debug("tabsViewCtrl.panelsUpdated " + panels.length)
+      $scope.panels = panels
+
+      if (!$scope.view_tab && panels.length > 0 && $scope.panels[$scope.panels.length - 1].name.startsWith("intro")) {
+        //  $scope.changeTab($scope.panels[0].name)
+      }
+
+      // if /#/service/{servicename} - change the tab
+      if ($scope.servicename) {
+        //   $scope.changeTab($scope.servicename)
+      }
     }
 
     /**
      * go to a service tab
      * direction - reverse or null (forward)
      */
-    $scope.changeTab = function(tab) {
-        tab = mrl.getFullName(tab)
-        $scope.view_tab = tab
-        $scope.history.push(tab)
-
-        // $location.path('service/' + tab, false)
-        // $state.go('tabs2','/service/' + tab)
-
-        //         $state.transitionTo('tabs2', {id: tab}, {
-        //             location: true,
-        //             inherit: true,
-        //             relative: $state.$current,
-        //             notify: false
-        //         })
-
-        $state.go('tabs2', {
-            servicename: tab
-        }, {
-            notify: false,
-            reload: false
-        })
-        //        $state.go('tabs2', { servicename: tab }, {notify:false, reload:true})
-        // $state.go($state.current, {}, {reload: true})
-        /*
+    $scope.changeTab = function (tab) {
+      tab = mrl.getFullName(tab)
+      $scope.view_tab = tab
+      $scope.history.push(tab)
+
+      // $location.path('service/' + tab, false)
+      // $state.go('tabs2','/service/' + tab)
+
+      //         $state.transitionTo('tabs2', {id: tab}, {
+      //             location: true,
+      //             inherit: true,
+      //             relative: $state.$current,
+      //             notify: false
+      //         })
+
+      $state.go(
+        "tabs2",
+        {
+          servicename: tab,
+        },
+        {
+          notify: false,
+          reload: false,
+        }
+      )
+      //        $state.go('tabs2', { servicename: tab }, {notify:false, reload:true})
+      // $state.go($state.current, {}, {reload: true})
+      /*
         $state.transitionTo('tabs2', {
             id: newId
         }, {
@@ -79,23 +93,27 @@ angular.module('mrlapp.mrl').controller('tabsViewCtrl', ['$location', '$scope',
         })*/
     }
 
-    $scope.goBack = function(){
-        // pop self
-        $scope.history.pop()
-        // go back one
-        tab = $scope.history[$scope.history.length - 1]
-        $scope.view_tab = tab
-
-        $state.go('tabs2', {
-            servicename: tab
-        }, {
-            notify: false,
-            reload: false
-        })
-        
+    $scope.goBack = function () {
+      // pop self
+      $scope.history.pop()
+      // go back one
+      tab = $scope.history[$scope.history.length - 1]
+      $scope.view_tab = tab
+
+      $state.go(
+        "tabs2",
+        {
+          servicename: tab,
+        },
+        {
+          notify: false,
+          reload: false,
+        }
+      )
     }
 
-    $scope.searchText = {// displayName: ""
+    $scope.searchText = {
+      // displayName: ""
     }
 
     $scope.hasNewStatus = true
@@ -105,5 +123,5 @@ angular.module('mrlapp.mrl').controller('tabsViewCtrl', ['$location', '$scope',
     mrl.setSearchFunction($scope.setSearchText)
     mrl.setTabsViewCtrl(this)
     mrl.subscribeToUpdates(panelsUpdated)
-}
+  },
 ])

From fef970145ea9f6894be66d8caa4e2088e21a898e Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Fri, 26 Apr 2024 12:20:58 -0700
Subject: [PATCH 118/131] quick-audio-player-fixes

---
 .../org/myrobotlab/audio/PlaylistPlayer.java  |  5 +++++
 .../WebGui/app/service/js/AudioFileGui.js     | 14 ++-----------
 .../app/service/views/AudioFileGui.html       | 20 +++++++++----------
 3 files changed, 17 insertions(+), 22 deletions(-)

diff --git a/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java b/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
index 46396bb50b..7ff3d128f0 100644
--- a/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
+++ b/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
@@ -4,10 +4,14 @@
 import java.util.Collections;
 import java.util.List;
 
+import org.myrobotlab.logging.LoggerFactory;
 import org.myrobotlab.service.AudioFile;
+import org.slf4j.Logger;
 
 public class PlaylistPlayer implements Runnable {
 
+  static final Logger log = LoggerFactory.getLogger(PlaylistPlayer.class);
+  
   private transient AudioFile audioFile = null;
   private transient Thread player;
   private boolean shuffle;
@@ -34,6 +38,7 @@ public void run() {
         audioFile.play(list.get(i), true, null, track);
       }
       if (!repeat) {
+        log.info("finished playing playlist");
         done = true;
       }
     }
diff --git a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
index 2a34217609..2510678548 100644
--- a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
@@ -8,19 +8,9 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl',
     // playing paused stopped
     $scope.activity = null
 
-    // $scope.playFile = function() {        
-    //     msg.send('playFile', $scope.selectedFile)
-    // }
-
     $scope.play = function() {
-        // if (blah){
-        // $scope.selectedFile = selectedFiles[0]    
-        // } else {
-        //     $scope.selectedFile = selectedFiles[0]    
-        // }
         let playFile = $scope.selectedFile
         msg.send('play', $scope.selectedFile)
-
     }
 
     $scope.setSelectedFileFromTrack = function(selected) {
@@ -29,7 +19,7 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl',
 
     $scope.startPlaylist = function() {
         if ($scope.selectedPlaylist) {
-            msg.send('startPlaylist', $scope.selectedPlaylist[0])
+            msg.send('startPlaylist', $scope.selectedPlaylist)
         } else {
             msg.send('startPlaylist')
         }
@@ -37,7 +27,7 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl',
 
     $scope.stopPlaylist = function() {
         if ($scope.selectedPlaylist) {
-            msg.send('stopPlaylist', $scope.selectedPlaylist[0])
+            msg.send('stopPlaylist', $scope.selectedPlaylist)
             msg.send('stop')
         } else {
             msg.send('stopPlaylist')
diff --git a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
index ffd479fc41..77304e74f9 100644
--- a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
@@ -23,10 +23,10 @@
                 <button class="btn btn-default" ng-click="msg.setMute(!service.mute);msg.broadcastState()">
                     <span class="glyphicon glyphicon-volume-off" title="mute track"/>
                 </button>
-                <button class="btn btn-default" ng-click="msg.setVolume(service.volume - 0.01);msg.broadcastState()">
+                <button class="btn btn-default" ng-click="msg.setVolume(service.config.volume - 0.01);msg.broadcastState()">
                     <span class="glyphicon glyphicon-volume-down" title="pump up the volume"/>
                 </button>
-                <button class="btn btn-default" ng-click="msg.setVolume(service.volume + 0.01);msg.broadcastState()">
+                <button class="btn btn-default" ng-click="msg.setVolume(service.config.volume + 0.01);msg.broadcastState()">
                     <span class="glyphicon glyphicon-volume-up" title="pump up the volume"/>
                 </button>
             </td>
@@ -47,7 +47,7 @@
         </tr>
         <tr>
             <td>volume</td>
-            <td>{{service.volume.toFixed(2) * 100}}</td>
+            <td>{{service.config.volume.toFixed(2) * 100}}</td>
         </tr>
         <tr>
             <td>peak volume multiplier</td>
@@ -69,7 +69,7 @@
     <br/>
     <!--Selected playlist: {{service.currentPlaylist}}<br/>-->
     <table border="1" class="table table-hover table-condensed table-striped table-bordered">
-        <!--tr ng-repeat="(name, fileset) in service.playlists">
+        <!--tr ng-repeat="(name, fileset) in service.config.playlists">
             <td>{{name}}{{fileset}}</td>
         </tr-->
         <tr>
@@ -79,10 +79,10 @@
                 <button class="btn btn-default" ng-click="msg.addPlaylist(service.currentPlaylist, directoryName);msg.broadcastState()">Add playlist</button>
                 <div class="form-group">
                     <label>select playlist:</label>
-                    {{selectedPlaylist[0]}}
+                    {{selectedPlaylist}}
                     
-                    <select multiple class="form-control vertical-menu" ng-change="setPlaylist(name)" ng-model="selectedPlaylist" id="service.playlists" title="select your playlist">
-                        <option ng-repeat="(name, fileset) in service.playlists" ng-value="{{name}}">{{name}}</option>
+                    <select size="8" class="form-control vertical-menu" ng-change="msg.send('setPlaylist', name)" ng-model="selectedPlaylist" id="service.config.playlists" title="select your playlist">
+                        <option ng-repeat="(name, fileset) in service.config.playlists" ng-value="{{name}}">{{name}}</option>
                     </select>
                 </div>
                 <button class="btn btn-default" ng-click="startPlaylist()">
@@ -91,10 +91,10 @@
                 <button class="btn btn-default" ng-click="stopPlaylist()">
                     <span class="glyphicon glyphicon-stop" title="stop play list"/>
                 </button>
-                <div ng-show="service.playlists" class="form-group">
+                <div ng-show="service.config.playlists" class="form-group">
                     <label>select file:</label>
-                    <select multiple class="form-control vertical-menu" ng-model="selectedFiles" ng-change="setSelectedFileFromTrack(selectedFiles[0])" title="select your track">
-                        <option ng-repeat="(name, fileset) in service.playlists[selectedPlaylist]" ng-value="{{fileset}}">{{fileset}}</option>
+                    <select size="8" class="form-control vertical-menu" ng-model="selectedFiles" ng-change="setSelectedFileFromTrack(selectedFiles)" title="select your track">
+                        <option ng-repeat="(name, fileset) in service.config.playlists[selectedPlaylist] track by $index" ng-value="fileset">{{fileset}}</option>
                     </select>
                 </div>
             </td>

From 6ed9606e749256eb9f5361d930be91ab1aed10f6 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Fri, 26 Apr 2024 15:53:59 -0700
Subject: [PATCH 119/131] full-feature-jukebox

---
 .../org/myrobotlab/audio/PlaylistPlayer.java  | 18 +++++++++++--
 .../org/myrobotlab/service/AudioFile.java     |  4 +++
 .../service/config/AudioFileConfig.java       | 10 ++++++++
 .../WebGui/app/service/js/AudioFileGui.js     | 22 +++++++++++-----
 .../app/service/views/AudioFileGui.html       | 25 ++++++++++++++++---
 5 files changed, 68 insertions(+), 11 deletions(-)

diff --git a/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java b/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
index 7ff3d128f0..8ee40c5583 100644
--- a/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
+++ b/src/main/java/org/myrobotlab/audio/PlaylistPlayer.java
@@ -25,8 +25,7 @@ public PlaylistPlayer(AudioFile audioFile) {
   }
 
   @Override
-  public void run() {
-
+  public void run() {    
     while (!done) {
 
       List<String> list = playlist;
@@ -54,9 +53,24 @@ private List<String> shuffle(List<String> list) {
 
   public synchronized void stop() {
     done = true;
+    audioFile.stop();
+    if (player != null) {
+      player.interrupt();
+    }
+  }
+  
+  public synchronized void skip() {
+    if (player != null) {
+      audioFile.stop();
+    }
   }
 
+
   public synchronized void start(List<String> playlist, boolean shuffle, boolean repeat, String track) {
+    
+    audioFile.getConfig().repeat = repeat;
+    audioFile.getConfig().shuffle = shuffle;
+    
     if (player != null) {
       audioFile.warn("playlist player already playing a list - stop before starting a new playlist");
       return;
diff --git a/src/main/java/org/myrobotlab/service/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java
index c383cc586c..33cf606d22 100644
--- a/src/main/java/org/myrobotlab/service/AudioFile.java
+++ b/src/main/java/org/myrobotlab/service/AudioFile.java
@@ -167,6 +167,7 @@ public void stopService() {
       p.stopPlaying();
       p.interrupt();
     }
+    playlistPlayer.stop();
   }
 
   public AudioData play(String filename) {
@@ -531,6 +532,9 @@ public void stopPlaylist() {
     playlistPlayer.stop();
   }
 
+  public void skip() {
+    playlistPlayer.skip();
+  }
 
   public double publishPeak(double peak) {
     log.debug("publishPeak {}", peak);
diff --git a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
index d0ab902aee..6901626661 100644
--- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
+++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java
@@ -12,6 +12,16 @@ public class AudioFileConfig extends ServiceConfig {
   
   public String currentPlaylist = "default";
   
+  /**
+   * randomly shuffles a play list
+   */
+  public boolean shuffle = false;
+  
+  /**
+   * repeats a playlist
+   */
+  public boolean repeat = false;
+  
   @Deprecated /* temporal variable */
   public String currentTrack = null;  
 
diff --git a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
index 2510678548..989556d08e 100644
--- a/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/AudioFileGui.js
@@ -4,6 +4,7 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl',
     var msg = this.msg
     $scope.peak = 0
     $scope.peakMax = 0
+    var firstUpdate = true
 
     // playing paused stopped
     $scope.activity = null
@@ -19,26 +20,35 @@ angular.module('mrlapp.service.AudioFileGui', []).controller('AudioFileGuiCtrl',
 
     $scope.startPlaylist = function() {
         if ($scope.selectedPlaylist) {
-            msg.send('startPlaylist', $scope.selectedPlaylist)
+            msg.send('startPlaylist', $scope.selectedPlaylist, $scope.service.config.shuffle, $scope.service.config.repeat)
         } else {
             msg.send('startPlaylist')
         }
+    } 
+    $scope.skip = function() {
+            msg.send('skip')
     }
 
     $scope.stopPlaylist = function() {
-        if ($scope.selectedPlaylist) {
-            msg.send('stopPlaylist', $scope.selectedPlaylist)
-            msg.send('stop')
-        } else {
             msg.send('stopPlaylist')
             msg.send('stop')
-        }
+    }
+
+    $scope.setPlaylist = function(name){
+        console.info('setPlaylist ' + name)
+        msg.send('setPlaylist', name)
     }
 
     // GOOD TEMPLATE TO FOLLOW
     this.updateState = function(service) {
         $scope.service = service
         $scope.service.loudness = 20
+
+        if (firstUpdate){
+            $scope.selectedPlaylist = $scope.service.config.currentPlaylist
+            firstUpdate = false
+        }
+        
         if (!$scope.selectedFile) {
 
             if (service.lastPlayed) {
diff --git a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
index 77304e74f9..cc488c620b 100644
--- a/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
+++ b/src/main/resources/resource/WebGui/app/service/views/AudioFileGui.html
@@ -1,3 +1,10 @@
+    <style>
+        .active-toggle {
+            background-color: #286090; /* Blue color for active state */
+            color: white;
+        }
+    </style>
+
 <div class="col-md-6">
     <table border="1" class="table table-hover table-condensed table-striped table-bordered">
         <tr>
@@ -81,15 +88,27 @@
                     <label>select playlist:</label>
                     {{selectedPlaylist}}
                     
-                    <select size="8" class="form-control vertical-menu" ng-change="msg.send('setPlaylist', name)" ng-model="selectedPlaylist" id="service.config.playlists" title="select your playlist">
+                    <select size="8" class="form-control vertical-menu" ng-change="setPlaylist(selectedPlaylist)" ng-model="selectedPlaylist" id="service.config.playlists" title="select your playlist">
                         <option ng-repeat="(name, fileset) in service.config.playlists" ng-value="{{name}}">{{name}}</option>
                     </select>
                 </div>
                 <button class="btn btn-default" ng-click="startPlaylist()">
-                    <span class="glyphicon glyphicon-play" title="play list"/>
+                    <span class="glyphicon glyphicon-play" title="Start Play List"/>
                 </button>
                 <button class="btn btn-default" ng-click="stopPlaylist()">
-                    <span class="glyphicon glyphicon-stop" title="stop play list"/>
+                    <span class="glyphicon glyphicon-stop" title="Stop Play List"/>
+                </button>
+
+                <button class="btn btn-default" title="Skip" ng-click="skip()">
+                    <span class="glyphicon glyphicon-forward"></span>
+                </button>                
+                <button class="btn btn-default" ng-click="service.config.shuffle = !service.config.shuffle" 
+                        ng-class="{'active-toggle': service.config.shuffle}">
+                    <span class="glyphicon glyphicon-random" title="Shuffle Playlist"></span> 
+                </button>
+                <button class="btn btn-default" ng-click="service.config.repeat = !service.config.repeat"
+                        ng-class="{'active-toggle': service.config.repeat}">
+                    <span class="glyphicon glyphicon-refresh" title="Repeat Playlist"></span>
                 </button>
                 <div ng-show="service.config.playlists" class="form-group">
                     <label>select file:</label>

From 077f8b92837a1ceb15b3dfdda2f4351e9d243d37 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 07:48:01 -0700
Subject: [PATCH 120/131] default disable InMoov2.py

---
 TODO.md                                       | 10 +++++++++
 .../java/org/myrobotlab/service/InMoov2.java  | 22 ++++++++++++++++++-
 .../service/config/InMoov2Config.java         |  5 ++++-
 3 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/TODO.md b/TODO.md
index 93df2377f6..09f7492a96 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,16 @@
 ## TODO
 
+- prompt - gets generated with system info, therby creating "perception" e.g. proximity sensor 2, classification human, position 30
+  , if a human is nearby assume its <star/> - can answer the question ... where are you ... ("you are just to the left of me")
+- doesnt look like it defaults to Intro !?!?
+- runtime should say what version java is running and warn if not valid
+- lower volume or change boot up sound
 - current config name doesn't show up in runtime
 - initCheckUp.py isn't getting run
 - peak is not working or implemented in the UI
 - peak isn't default
+
+## DONE
+
+- a delete config button - should do a move to trash directory with a datetimestamp
+- dot dot on Runtime... platform info is late, or doesn't get published should be x86 64
diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 269d9dc8db..130c24f2c4 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -1565,7 +1565,7 @@ public String publishFlash(String flashName) {
    * onHeartbeat at a regular interval
    */
   public Heartbeat publishHeartbeat() {
-    log.info("publishHeartbeat");
+    log.debug("publishHeartbeat");
     heartbeatCount++;
     Heartbeat heartbeat = new Heartbeat(this);
     try {
@@ -2295,4 +2295,24 @@ public void waitTargetPos() {
     sendToPeer("torso", "waitTargetPos");
   }
 
+  public void foundPerson(String name) {
+    foundPerson(name, 1.0);
+  }
+  
+  public void foundPerson(String name, Double confidence) {
+    if (confidence == null) {
+      confidence = 1.0;
+    }
+    Map<String, Object> data = new HashMap<>();
+    data.put("name", name);
+    data.put("confidence", confidence);
+    invoke("publishFoundPerson", data);
+  }
+  
+  
+  public Map<String, Object> publishFoundPerson(Map<String, Object> data) {
+    return data;
+  }
+  
+  
 }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index cebe3ea063..98a593a2d3 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -154,7 +154,10 @@ public class InMoov2Config extends ServiceConfig {
 
   public boolean virtual = false;
 
-  public boolean execScript = true;
+  /**
+   * false for now to not interfere
+   */
+  public boolean execScript = false;
 
   public InMoov2Config() {
   }

From 605a163113553b0208ccd597b2a1f4171c33d81d Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 08:14:52 -0700
Subject: [PATCH 121/131] formatting

---
 .../java/org/myrobotlab/service/InMoov2.java  | 56 ++++++++++---------
 1 file changed, 30 insertions(+), 26 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 130c24f2c4..7448db6d46 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -244,7 +244,7 @@ public void addTextListener(TextListener service) {
   public InMoov2Config apply(InMoov2Config c) {
     super.apply(c);
     try {
-      
+
       if (c.locale != null) {
         setLocale(c.locale);
       } else {
@@ -314,7 +314,7 @@ synchronized public void boot() {
         log.warn("will not boot again");
         return;
       }
-      
+
       if (bootCount == 0) {
         info("%s BOOTING ....", getName());
       }
@@ -326,28 +326,28 @@ synchronized public void boot() {
         info("BOOT runtime still processing config set %s, waiting ....", runtime.getConfigName());
         return;
       }
-      
+
       if (!isReady()) {
         info("BOOT %s is not yet ready, waiting ....", getName());
         return;
       }
-      
+
       info("BOOT starting mandatory services");
-      
+
       try {
         // This is required the core of InMoov is
         // a FSM ProgramAB and some form of Python/Jython
         startPeer("fsm");
 
         // Chatbot is a required part of InMoov2
-        ProgramAB chatBot = (ProgramAB)startPeer("chatBot");
+        ProgramAB chatBot = (ProgramAB) startPeer("chatBot");
         chatBot = (ProgramAB) startPeer("chatBot");
         chatBot.startSession();
         chatBot.setPredicate("robot", getName());
       } catch (IOException e) {
         error(e);
       }
-      
+
       // InMoov2 is now "ready" for mandatory synchronous processing
       info("BOOT starting scripts");
 
@@ -763,7 +763,7 @@ public void finishedGesture(String nameOfGesture) {
    * @param event
    */
   public void fire(String event) {
-    FiniteStateMachine fsm =(FiniteStateMachine)getPeer("fsm");
+    FiniteStateMachine fsm = (FiniteStateMachine) getPeer("fsm");
     if (fsm != null) {
       fsm.fire(event);
     } else {
@@ -861,9 +861,9 @@ public OpenCV getOpenCV() {
   }
 
   public String getPredicate(String key) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
-    return getPredicate(chatBot.getConfig().currentUserName, key);
+      return getPredicate(chatBot.getConfig().currentUserName, key);
     } else {
       log.info("chatBot not ready");
       return null;
@@ -871,10 +871,10 @@ public String getPredicate(String key) {
   }
 
   public String getPredicate(String user, String key) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
 
-    return chatBot.getPredicate(user, key);
+      return chatBot.getPredicate(user, key);
     } else {
       log.info("chatBot not ready");
       return null;
@@ -888,14 +888,14 @@ public String getPredicate(String user, String key) {
    * @return
    */
   public Response getResponse(String text) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
 
-    Response response = chatBot.getResponse(text);
-    return response;
+      Response response = chatBot.getResponse(text);
+      return response;
     } else {
       log.info("chatBot not ready");
-      return null;      
+      return null;
     }
   }
 
@@ -1074,7 +1074,7 @@ public boolean accept(File dir, String name) {
 
       if (files != null) {
         for (File file : files) {
-          
+
           Python p = (Python) Runtime.getService("python");
           if (p != null) {
             p.execFile(file.getAbsolutePath());
@@ -1442,7 +1442,10 @@ public void onText(String text) {
   }
 
   // TODO FIX/CHECK this, migrate from python land
-  @Deprecated /* these are fsm states and should be implemented in python callbacks */
+  @Deprecated /*
+               * these are fsm states and should be implemented in python
+               * callbacks
+               */
   public void powerDown() {
 
     rest();
@@ -1461,7 +1464,10 @@ public void powerDown() {
 
   // TODO FIX/CHECK this, migrate from python land
   // FIXME - defaultPowerUp switchable + override
-  @Deprecated /* these are fsm states and should be implemented in python callbacks */
+  @Deprecated /*
+               * these are fsm states and should be implemented in python
+               * callbacks
+               */
   public void powerUp() {
     enable();
     rest();
@@ -2025,7 +2031,7 @@ public boolean setPirPlaySounds(boolean b) {
   }
 
   public Object setPredicate(String key, Object data) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (data == null) {
       chatBot.setPredicate(key, null); // "unknown" "null" other sillyness ?
     } else {
@@ -2090,7 +2096,7 @@ public boolean setSpeechType(String speechType) {
   }
 
   public void setTopic(String topic) {
-    ProgramAB chatBot = (ProgramAB)getPeer("chatBot");
+    ProgramAB chatBot = (ProgramAB) getPeer("chatBot");
     if (chatBot != null) {
       chatBot.setTopic(topic);
     } else {
@@ -2298,7 +2304,7 @@ public void waitTargetPos() {
   public void foundPerson(String name) {
     foundPerson(name, 1.0);
   }
-  
+
   public void foundPerson(String name, Double confidence) {
     if (confidence == null) {
       confidence = 1.0;
@@ -2308,11 +2314,9 @@ public void foundPerson(String name, Double confidence) {
     data.put("confidence", confidence);
     invoke("publishFoundPerson", data);
   }
-  
-  
+
   public Map<String, Object> publishFoundPerson(Map<String, Object> data) {
     return data;
   }
-  
-  
+
 }

From 3c92a56a79bbbc353c1861ed5db24fcfe377bdb9 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 08:42:17 -0700
Subject: [PATCH 122/131] removed terminalmanager

---
 .../resources/resource/TerminalManager.png    | Bin 1937 -> 0 bytes
 .../app/service/js/TerminalManagerGui.js      |  72 -------------
 .../app/service/views/TerminalManagerGui.html | 102 ------------------
 3 files changed, 174 deletions(-)
 delete mode 100644 src/main/resources/resource/TerminalManager.png
 delete mode 100644 src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
 delete mode 100644 src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html

diff --git a/src/main/resources/resource/TerminalManager.png b/src/main/resources/resource/TerminalManager.png
deleted file mode 100644
index e212bb9b2d00a7208a1173f2514387b218bee6de..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1937
zcmV;C2X6R@P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf2PR2GK~!i%otQa`
zRoxPXSGV(kqNs_nU~FjUKQI;yG*B=T%#B2IH8n61L{OXpQN%G2LopKs13^@rXZ!1C
zU7vm5uHNU|&-+4k)v9?-d+$Co_wV1Io0*wuBIUWcxj)i>fNe~dsP=!0%&k0D52StO
z*|TSJt5>g{T3F|llY`Wa;os)?Tg=?Od$;d^NfI@=vW3B<*lC!Q*PPSx>921;LS352
z=FOYivSrH_B(2CIV+E_d=9!ciXftjs9=v}2y8Za^qsue*?%kW4ot<s#)~##%_wR2{
zpFSNX(H-VFb`$<BvXL6Upt*TDTg9e5`P;W|+qG-g+Qy9=+pk~0{(FShuV3Hx?c3Kr
zefrdvELpOkn8|9wX~4(E$Nbg({{6dik-o}u^3C7bcJ=Djwqe7DK7m;sFj`ROrAwD~
zVnXaf1-@A%3D8`Q;po&)YTg(rb@r)l=qep)mc}_}%a<>2Jf&8wSkaFbHWLVwPa<H`
zV3G~W$yFVc{$Q3t1;$OAHnj^EF0}32xA*bNl`GrYwQJj|Rjc~s+$|^zOw*~iMvnCd
z+N7$>+Oz!OEI~Q~R)5>JZSBmNGwuBO^KI9zT|JN;aS~o`V8SuV$$)9ij@dFk3H^eD
z?aaXY_wU>8-MicE+qc`+ty|mCqet7VTesStJ$rid`J^HdFt!#1IhN87WFB>iF<svQ
z4RDgU8*kpcX~&Ko>rHc<&>uW_u$?=1u6_CPWtafS9F?bBCzk}O-LV`{mtGk62>=ur
zG67?VN9)U%FWb9!@47F?NS{i?Lm+dhPhU{S(-uWA#t+^nU_qggN$uUcw_Ut=u^l>e
zsBPJ@rCqvosq47Z#yQc1MKk|2AN`Wlr;l(v>;SL;Ixgz`ORgUuF7)Zsr~5@de*Abp
zS`^^@z+I5?osrEVTTrSE0%Ph#0lL&1H|zie9P`}g=o~q6r0v|fvz<MAwx4?@1Cb0L
z-Vr=r*Q{C7$@c|z8?@PJ&{x>3{QLLsy&qt1@(!2SVC7e@UbP)NcC?!}Z+1P+fBg8d
zbB-31z>gJY<$X6`V*=;}dW!;i!batQ8Z^xl@$1*GJ-0B&aD?ClyBh@4h%=}0Z{NQ4
zBqZdi^!V}P_VD4uHvdK29z1x^9zA;0o;-OnT=Hxtn?|2DfsVSLKY#8I44WSFB_FhC
z2LKcJ`SWLQo~B7kB98(S<1$~re!X3}a;1MA@_}{v^5u5p#*H334Ox%o$jbt;Ff>p6
z*e8oH9-5hNV)z<8eE4vC@!~~qa1y+@1m_ElF_UD~&@*`&u~5RM@qv)lr*D<9vokCf
zb%`6@fjUkQL}Mfa$PzXt=#FI${2}z7YjzhOZ3*>>+7sj9bJ|q^H6ivSnqcKvZML3m
zhcnKV2j)Wm;lqdCJn?zSGq>a974#|%m)JttIKZAZ#t^|YBDo95=ETRd&E(7lQkIK4
zHeUsP{d3x8Kssj;;5dDZ7ud<gO7qy|IH9X~%ptG#G3W!g=V1rNFqRaK9<J;YgB|dg
zP@u*;mZvUdVGW_|g5lpMP*<_S<XONm<oV_<*mxEOV>*DY628Fv0DXcw=obA;IR=Yo
zazNvpZ_as7_Gc&b%Q!HJ1M?aK;OQe=Y;400=vWg78-iSME@s+Hj0}jc@>tG9+7mUF
zT2Q~l7&O)b0+|b0pD2=JkogknBhqh@JPnI}E6Ii&dpUE(6VnPa*8hLSX|Fjz^@WBV
z;BizRqnu}KJPNs-APdxH#F*ZK$`#c%Hc=?+(_5cF4W@BG(=^|m{^<0{MC!oUSY%Vx
z58GJI>XUl<0(}A!;QNGg&atuaG2maDs$)<a8}ox~2xy06tI!wQf(g(x=R6zFVu?w~
zg>m}mU*l^2v?tFPpsshSu_jB@Bvr1BRT;{K(Jr@k#;F_YYskrVAjm~FB%hQhIY2hP
z$`Hu>6_fPBUZG(JpxR&^>zBYs0@}?7DUZbh^b$}HQdebYmd7VE@?vCg1-48COe8Ws
z^}u*g<3Pp&?CCF%-GxO@p_D<&fN>_B)Q~oH$<r^Pt@>1dn)lt+?*LC9cK}$p20E^K
z;bcVmSDUCd<Q!)$_4W#1aESp<lM0R{);Gt>iPUGFvDh3>s>yd5yiWjP7f|RV#&i|L
z*QOG+iD~(2o-toPd-@Ub600PrCUKm}st=gd*rYh14UR3x73%C4KI{O*cRs}D1aIUV
zLn)7qCsF#5cgzM)TOF%&0uYm!Id$q(|6z+CW$)a%)1E(nzThX1{}*E`__wGz=#$U-
z0|yQalbJbr@?`hoHP3IFa2(z0u|~83R>Dt&ziAp@>nIyPCa2DSVY~jsi4$|II1V%e
z7Y2$PkJ&)#66p`ue-g&>*D(t>+yTxL%H+Vy0-+z19an#3DSdt7=`-eQ52;&h|JD8h
X?w=+{ykB|700000NkvXXu0mjf3k<|N

diff --git a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
deleted file mode 100644
index 17a85e6e53..0000000000
--- a/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js
+++ /dev/null
@@ -1,72 +0,0 @@
-angular.module("mrlapp.service.TerminalManagerGui", []).controller("TerminalManagerGuiCtrl", ["$scope", "mrl", function($scope, mrl) {
-    console.info("TerminalManagerGuiCtrl")
-    var _self = this
-    var msg = this.msg
-
-    $scope.processCommand = function(key, input) {
-        msg.send("processCommand", key, input)
-        $scope.service.inputValue = ""
-    }
-
-    this.onMsg = function(inMsg) {
-        let data = inMsg.data[0]
-        switch (inMsg.method) {
-        case "onState":
-            $scope.service = data
-            $scope.$apply()
-            break
-        case "onLog":
-            $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output + data.msg
-            let length = $scope.service.terminals[data.terminal].output.length
-            if (length > 1024) {
-                let overLength = length - 1024;
-                $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output.substring(overLength);
-            }
-            // $scope.$apply()
-            $scope.$apply(function() {
-                    // Scroll logic here
-                    // Assuming you can uniquely identify the <pre> for this terminal
-                    let terminalElement = document.querySelector('.terminal-wrapper[data-terminal-id="' + data.terminal + '"] .terminal2');
-                    if (terminalElement) {
-                        terminalElement.scrollTop = terminalElement.scrollHeight;
-                    }
-                });
-                
-            break
-        case "onStdOut":
-            break
-        case "onCmd":
-            // FIXME - keep a list of commands ... can support history and maybe more importantly 
-            // script generation to make automated packages
-            $scope.service.terminals[data.terminal].output = $scope.service.terminals[data.terminal].output + '# ' + data.cmd    
-            $scope.$apply()
-            break
-        default:
-            console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
-            break
-        }
-    }
-
-    // Assuming `service` is your service managing terminals
-    $scope.startTerminal = function(key) {
-        msg.send("startTerminal", key)
-    }
-
-    $scope.terminateTerminal = function(key) {
-        msg.send("terminateTerminal", key)
-    }
-
-    $scope.saveTerminal = function(key) {
-        msg.send("saveTerminal", key)
-    }
-
-    $scope.deleteTerminal = function(key) {
-        msg.send("deleteTerminal", key)
-    }
-
-    msg.subscribe("publishLog")
-    // msg.subscribe("publishStdOut")
-    msg.subscribe("publishCmd")
-    msg.subscribe(this)
-}
-, ])
diff --git a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
deleted file mode 100644
index fe42096ccb..0000000000
--- a/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html
+++ /dev/null
@@ -1,102 +0,0 @@
-<style>
-    .inline-buttons {
-        display: inline-block;
-        /* Or display: inline; */
-    }
-
-    .terminal2 {
-        background-color: black;
-        color: #33ff33;
-        font-family: 'Courier New', Courier, monospace;
-        font-size: 0.8em;
-        border-radius: 0;
-        margin: 0;
-        /* Removes default margin */
-        padding: 10px;
-        /* Adjust based on your design needs */
-        border: none;
-        /* Removes border */
-        overflow: auto;
-    }
-
-    .terminal-wrapper {
-          max-height: 800px; /* Adjust based on your needs */
-  overflow-y: auto;
-        display: flex;
-        flex-direction: column;
-        align-items: stretch;
-        /* Ensures child elements fill the container */
-    }
-
-    .terminal-input {
-        background-color: black;
-        color: #33ff33;
-        border: none;
-        /* Removes border */
-        outline: none;
-        /* Removes focus outline */
-        font-family: 'Courier New', Courier, monospace;
-        font-size: 0.8em;
-        padding: 10px;
-        /* Should match the <pre> padding for alignment */
-        width: 100%;
-        /* Ensures it takes up all available width */
-        box-sizing: border-box;
-        /* Includes padding in the width calculation */
-        margin: 0;
-        /* Removes default margin */
-    }
-</style>
-<table class="table table-striped">
-    <thead>
-        <tr>
-            <th></th>
-            <th></th>
-            <th></th>
-            <th>Name</th>
-            <th>PID</th>
-            <th>Shell</th>
-            <th>Command</th>
-            <th>Control</th>
-        </tr>
-    </thead>
-    <tr ng-repeat="(key, value) in service.terminals">
-        <td>
-            <input type="radio" name="selectedTerminal" ng-model="ctrl.selectedTerminal" ng-value="terminal"/>
-        </td>
-        <td>
-            <img src="TerminalManager.png" width="16"/>
-        </td>
-        <td>
-            <img ng-src="{{value.isRunning ? 'connected.png' : 'disconnected.png'}}" alt="Connection Status" width="16"/>
-        </td>
-        <td>
-            <small>{{key}}</small>
-        </td>
-        <td>
-            <small>{{value.pid}}</small>
-        </td>
-        <td>
-            <small>{{value.shellCommand}}</small>
-        </td>
-        <td>
-            <small>{{value.lastInput}}</small>
-        </td>
-        <td>
-            <span class="inline-buttons">
-                <button class="btn btn-sm" ng-click="startTerminal(key)">Start</button>
-                <button class="btn btn-sm" ng-click="terminateTerminal(key)">Terminate</button>
-                <button class="btn btn-sm" ng-click="saveTerminal(key)">Save</button>
-                <button class="btn btn-sm" ng-click="deleteTerminal(key)">Delete</button>
-            </span>
-        </td>
-    </tr>
-</table>
-
-<div class="terminal-wrapper" ng-repeat="(key, value) in service.terminals" data-terminal-id="{{key}}">
-    {{key}}<button class="btn btn-sm" ng-click="deleteTerminal(key)">Clear</button>
-    
-    <pre class="terminal2" id="terminal-{{key}}" >{{value.output}}</pre>
-    
-    <input class="terminal-input" type="text" class="form-control" ng-model="service.inputValue" ng-keyup="$event.keyCode == 13 ? processCommand(key, service.inputValue) : null" placeholder="type here...">
-</div>
\ No newline at end of file

From 3db187276c98e7a51faf95e584bfda423892ffdf Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 08:47:45 -0700
Subject: [PATCH 123/131] fixed white space

---
 src/main/java/org/myrobotlab/service/Runtime.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 4b5f418a39..79439eea08 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -3687,10 +3687,10 @@ public static Double getBatteryLevel() {
         // TODO This is incorrect, will not work when unplugged
         // and acpitool output is different than expected,
         // at least on Ubuntu 22.04 - consider oshi library
-        if (FileIO.isExecutableAvailable("acpi")) {
+        if (FileIO.isExecutableAvailable("acpi")) {          
           String ret = Runtime.execute("acpi");
           int pos0 = ret.indexOf("%");
-
+  
           if (pos0 != -1) {
             int pos1 = ret.lastIndexOf(" ", pos0);
             // int pos1 = ret.indexOf("%", pos0);

From 65e760ae305aa90c7f1dfd60b2b29739ad35cd24 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 08:50:31 -0700
Subject: [PATCH 124/131] reformat vertx

---
 src/main/java/org/myrobotlab/service/Vertx.java | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java
index a47d75a954..c8e75a62ff 100644
--- a/src/main/java/org/myrobotlab/service/Vertx.java
+++ b/src/main/java/org/myrobotlab/service/Vertx.java
@@ -85,8 +85,7 @@ public void start() {
 
     if (config.autoStartBrowser) {
       log.info("auto starting default browser");
-      String startUrl = (String.format((config.ssl) ? "https:" : "http:")
-          + String.format("//localhost:%d/index.html", config.port));
+      String startUrl = (String.format((config.ssl) ? "https:" : "http:") + String.format("//localhost:%d/index.html", config.port));
       BareBonesBrowserLaunch.openURL(startUrl);
     }
     listening = true;

From a8abb8df9413b39f0aae72df0c372d5e04455303 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 08:52:20 -0700
Subject: [PATCH 125/131] reformat vertx

---
 src/main/java/org/myrobotlab/service/Vertx.java | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java
index c8e75a62ff..757f220ff1 100644
--- a/src/main/java/org/myrobotlab/service/Vertx.java
+++ b/src/main/java/org/myrobotlab/service/Vertx.java
@@ -78,9 +78,7 @@ public void start() {
      * </pre>
      */
 
-    // vertx = io.vertx.core.Vertx.vertx(new
-    // VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));
-    vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000));
+    // vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));    vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000));
     vertx.deployVerticle(new ApiVerticle(this));
 
     if (config.autoStartBrowser) {

From 58d0889aaa9c1b80a3c7d306b9364a90a99d679b Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 09:00:31 -0700
Subject: [PATCH 126/131] vertx formatting

---
 src/main/java/org/myrobotlab/service/Vertx.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/myrobotlab/service/Vertx.java b/src/main/java/org/myrobotlab/service/Vertx.java
index 757f220ff1..d51deeffb1 100644
--- a/src/main/java/org/myrobotlab/service/Vertx.java
+++ b/src/main/java/org/myrobotlab/service/Vertx.java
@@ -78,7 +78,8 @@ public void start() {
      * </pre>
      */
 
-    // vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));    vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000));
+    // vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setWorkerPoolSize(125).setBlockedThreadCheckInterval(100000));
+    vertx = io.vertx.core.Vertx.vertx(new VertxOptions().setBlockedThreadCheckInterval(100000));
     vertx.deployVerticle(new ApiVerticle(this));
 
     if (config.autoStartBrowser) {

From 35704ac49d82ca87fb513846d72c9ad2f4061b23 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sat, 27 Apr 2024 09:01:58 -0700
Subject: [PATCH 127/131] finally formatting fixed

---
 .../java/org/myrobotlab/service/Runtime.java  | 34 ++++++++++---------
 1 file changed, 18 insertions(+), 16 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/Runtime.java b/src/main/java/org/myrobotlab/service/Runtime.java
index 2185727eb5..faf505a2c4 100644
--- a/src/main/java/org/myrobotlab/service/Runtime.java
+++ b/src/main/java/org/myrobotlab/service/Runtime.java
@@ -3684,24 +3684,26 @@ public static Double getBatteryLevel() {
         }
 
       } else if (platform.isLinux()) {
-        // TODO This is incorrect, will not work when unplugged
-        // and acpitool output is different than expected,
-        // at least on Ubuntu 22.04 - consider oshi library
-        String ret = Runtime.execute("acpi");
-        int pos0 = ret.indexOf("%");
-
-        if (pos0 != -1) {
-          int pos1 = ret.lastIndexOf(" ", pos0);
-          // int pos1 = ret.indexOf("%", pos0);
-          String dble = ret.substring(pos1, pos0).trim();
-          try {
-            r = Double.parseDouble(dble);
-          } catch (Exception e) {
-            log.error("no Battery detected by system");
+          // TODO This is incorrect, will not work when unplugged
+          // and acpitool output is different than expected,
+          // at least on Ubuntu 22.04 - consider oshi library
+          if (FileIO.isExecutableAvailable("acpi")) {
+          String ret = Runtime.execute("acpi");
+          int pos0 = ret.indexOf("%");
+  
+          if (pos0 != -1) {
+            int pos1 = ret.lastIndexOf(" ", pos0);
+            // int pos1 = ret.indexOf("%", pos0);
+            String dble = ret.substring(pos1, pos0).trim();
+            try {
+              r = Double.parseDouble(dble);
+            } catch (Exception e) {
+              log.error("no Battery detected by system");
+            }
+            return r;
           }
-          return r;
+          log.info(ret);
         }
-        log.info(ret);
       } else if (platform.isMac()) {
         String ret = Runtime.execute("pmset -g batt");
         int pos0 = ret.indexOf("Battery-0");

From 868480a7b30e4090bd3c39bf21d950d275c63383 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sun, 28 Apr 2024 08:02:54 -0700
Subject: [PATCH 128/131] audioPlayer and locales figured out

---
 .../java/org/myrobotlab/service/InMoov2.java  | 78 ++++++++++++++++++-
 .../service/config/InMoov2Config.java         | 10 +--
 .../WebGui/app/service/js/PollyGui.js         |  4 +
 3 files changed, 86 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/myrobotlab/service/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java
index 7448db6d46..5ff30ed3c6 100644
--- a/src/main/java/org/myrobotlab/service/InMoov2.java
+++ b/src/main/java/org/myrobotlab/service/InMoov2.java
@@ -36,6 +36,7 @@
 import org.myrobotlab.service.Log.LogEntry;
 import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis;
 import org.myrobotlab.service.config.InMoov2Config;
+import org.myrobotlab.service.data.AudioData;
 import org.myrobotlab.service.data.JoystickData;
 import org.myrobotlab.service.data.Locale;
 import org.myrobotlab.service.interfaces.IKJointAngleListener;
@@ -229,6 +230,8 @@ public static void main(String[] args) {
 
   protected String voiceSelected;
 
+  protected boolean pirActive = false;
+
   public InMoov2(String n, String id) {
     super(n, id);
     locales = Locale.getLocaleMap("en-US", "fr-FR", "es-ES", "de-DE", "nl-NL", "ru-RU", "hi-IN", "it-IT", "fi-FI", "pt-PT", "tr-TR");
@@ -402,7 +405,7 @@ synchronized public void boot() {
 
       if (config.startupSound) {
         String startupsound = FileIO.gluePaths(getResourceDir(), "/system/sounds/startupsound.mp3");
-        invoke("publishPlayAudioFile", startupsound);
+        playAudioFile(startupsound);
       }
 
       List<ServiceInterface> services = Runtime.getServices();
@@ -979,6 +982,11 @@ public boolean isMute() {
   public boolean isSpeaking() {
     return isSpeaking;
   }
+  
+  public boolean isPirActive() {
+    return pirActive;
+  }
+
 
   /**
    * execute python scripts in the app directory on startup of the service
@@ -1189,6 +1197,23 @@ public PredicateEvent onChangePredicate(PredicateEvent event) {
     // do defaults ?
     return event;
   }
+  
+  /**
+   * Subscription for audioPlayer starting to play a file.
+   * @param data
+   */
+  public void onAudioStart(AudioData data) {
+    processMessage("onAudioStart", data);
+  }
+  
+  /**
+   * Subscription for audioPlayer stopping an audio file.
+   * @param data
+   */
+  public void onAudioEnd(AudioData data) {
+    processMessage("onAudioEnd", data);
+  }
+
 
   /**
    * comes in from runtime which owns the config list
@@ -1371,6 +1396,12 @@ public boolean onSense(boolean b) {
     } else {
       invoke("publishEvent", "PIR OFF");
     }
+    
+    // Better - processed through a potentially configured "processor"
+    // "named" message since sender is this service
+    processMessage("onSense", b);
+    pirActive = b;
+    
     return b;
   }
 
@@ -1440,6 +1471,11 @@ public void onText(String text) {
     log.info("onText - {}", text);
     invoke("publishText", text);
   }
+  
+  public void playAudioFile(String filename) {
+    log.info("playAudioFile {}", filename);
+    invoke("publishPlayAudioFile", filename);
+  }
 
   // TODO FIX/CHECK this, migrate from python land
   @Deprecated /*
@@ -1484,6 +1520,34 @@ public void powerUp() {
   public void processMessage(String method) {
     processMessage(method, (Object[]) null);
   }
+  
+  public void playMusic() {
+    AudioFile af = (AudioFile) getPeer("audioPlayer");
+    if (af != null) {
+      af.startPlaylist();
+    }
+  }
+  
+  public void nextPlay() {
+    AudioFile af = (AudioFile) getPeer("audioPlayer");
+    if (af != null) {
+      af.skip();
+    }
+  }
+  
+  public void searchPlay(String requestedSong) {
+    AudioFile af = (AudioFile) getPeer("audioPlayer");
+    if (af != null) {
+      Map<String, List<String>> pls = af.getPlaylists();
+      for(String playlist: pls.keySet()) {
+        for (String song : pls.get(playlist)) {
+          if (song.contains(requestedSong)) {
+            af.play(song);
+          }
+        }
+      }
+    }
+  }
 
   /**
    * Will publish processing messages to the processor(s) currently subscribed.
@@ -2009,6 +2073,18 @@ public void setLocale(String code) {
     // super.setLocale(code);
     for (ServiceInterface si : Runtime.getLocalServices().values()) {
       if (!si.equals(this)) {
+        // by default, InMoov2 tries to set all Locales on all services
+        // from its configured Locale, or a default Locale set on the OS.
+        // This works ok when ProgramAB is providing translations, however,
+        // in the case of "brain" translation will be provided at a different
+        // layer, therefore resetting chatBot to en-US bot from configured "brain" bot
+        // is prevented
+        if (si instanceof ProgramAB) {
+          ProgramAB chatbot = (ProgramAB)si;
+          if ("brain".equals(chatbot.getCurrentBotName())){
+            continue;
+          }
+        }
         si.setLocale(code);
       }
     }
diff --git a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
index 98a593a2d3..d0a022b139 100644
--- a/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
+++ b/src/main/java/org/myrobotlab/service/config/InMoov2Config.java
@@ -154,9 +154,6 @@ public class InMoov2Config extends ServiceConfig {
 
   public boolean virtual = false;
 
-  /**
-   * false for now to not interfere
-   */
   public boolean execScript = false;
 
   public InMoov2Config() {
@@ -538,8 +535,7 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishStopAnimation", getPeerName("neoPixel")));
     // listeners.add(new Listener("publishProcessMessage",
     // getPeerName("python"), "onPythonMessage"));
-    listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));
-    
+    listeners.add(new Listener("publishProcessMessage", getPeerName("python"), "onPythonMessage"));    
     listeners.add(new Listener("publishPython", getPeerName("python")));
 
     // InMoov2 --to--> InMoov2
@@ -551,6 +547,10 @@ public Plan getDefault(Plan plan, String name) {
     listeners.add(new Listener("publishMoveTorso", getPeerName("torso"), "onMove"));
 
     // service --to--> InMoov2
+    AudioFileConfig audioPlayer = (AudioFileConfig) plan.get(getPeerName("audioPlayer"));
+    audioPlayer.listeners.add(new Listener("publishAudioStart", name));
+    audioPlayer.listeners.add(new Listener("publishAudioEnd", name));
+
     AudioFileConfig mouth_audioFile = (AudioFileConfig) plan.get(getPeerName("mouth.audioFile"));
     mouth_audioFile.listeners.add(new Listener("publishPeak", name));
 
diff --git a/src/main/resources/resource/WebGui/app/service/js/PollyGui.js b/src/main/resources/resource/WebGui/app/service/js/PollyGui.js
index f9e42cdb40..fcc6634646 100644
--- a/src/main/resources/resource/WebGui/app/service/js/PollyGui.js
+++ b/src/main/resources/resource/WebGui/app/service/js/PollyGui.js
@@ -33,6 +33,10 @@ angular.module('mrlapp.service.PollyGui', []).controller('PollyGuiCtrl', ['peer'
             $scope.spoken = data
             $scope.$apply()
             break
+        case 'onStatus':
+            $scope.status = data
+            $scope.$apply()
+            break            
         default:
             console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method)
             break

From 43f7677b09d7084f4f79849232b28738b7a20e09 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Sun, 28 Apr 2024 08:03:13 -0700
Subject: [PATCH 129/131] list

---
 TODO.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/TODO.md b/TODO.md
index 09f7492a96..912002262a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,14 +1,18 @@
 ## TODO
 
+- Make list of Input yolo, camera cv filters, pir, finger sensors ... into a context - the context construct a prompt -> perception
+- Convert all JukeBox or AudioFile.py to InMoov2
+- Make LanguageModel
+- Make ContextManager
 - prompt - gets generated with system info, therby creating "perception" e.g. proximity sensor 2, classification human, position 30
   , if a human is nearby assume its <star/> - can answer the question ... where are you ... ("you are just to the left of me")
-- doesnt look like it defaults to Intro !?!?
 - runtime should say what version java is running and warn if not valid
 - lower volume or change boot up sound
 - current config name doesn't show up in runtime
 - initCheckUp.py isn't getting run
 - peak is not working or implemented in the UI
 - peak isn't default
+- multiple sets of process id's on stale ui - fix by stablizing new randome one ?
 
 ## DONE
 

From 46a38304cb8be3bfaeaf0feca9567bb310a622b0 Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Tue, 30 Apr 2024 09:13:02 -0700
Subject: [PATCH 130/131] updated config

---
 TODO.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/TODO.md b/TODO.md
index 912002262a..ae9e523f54 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,5 +1,7 @@
 ## TODO
 
+- neopixel when attached doesn't show the attached controller
+- Implement robotCanMoveHeadWhileSpeaking with guards in a state (idle ?)
 - Make list of Input yolo, camera cv filters, pir, finger sensors ... into a context - the context construct a prompt -> perception
 - Convert all JukeBox or AudioFile.py to InMoov2
 - Make LanguageModel

From ef8793a8254c5e23bf21ffc16fe104b2eba7010b Mon Sep 17 00:00:00 2001
From: supertick <grog@myrobotlab.org>
Date: Wed, 1 May 2024 06:37:03 -0700
Subject: [PATCH 131/131] updated with javadoc, timeout, and default

---
 src/main/java/org/myrobotlab/io/FileIO.java | 78 ++++++++++++++-------
 1 file changed, 53 insertions(+), 25 deletions(-)

diff --git a/src/main/java/org/myrobotlab/io/FileIO.java b/src/main/java/org/myrobotlab/io/FileIO.java
index e027aeacd4..95c5134a3c 100644
--- a/src/main/java/org/myrobotlab/io/FileIO.java
+++ b/src/main/java/org/myrobotlab/io/FileIO.java
@@ -52,6 +52,7 @@
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
 import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
@@ -1427,34 +1428,61 @@ public static String normalize(String dirPath) {
     }
   }
   
+  /**
+   * Checks if a specific executable command is available in the system.
+   * This function attempts to execute the command with a timeout of 3 seconds to ensure
+   * that the check does not hang indefinitely. The process is considered available
+   * if it can be started without throwing an exception and completes successfully
+   * within the specified timeout.
+   * 
+   * @param command the command to check for availability.
+   * @return true if the command is available and completes successfully within the timeout, false otherwise.
+   */
+  
   public static boolean isExecutableAvailable(String command) {
-    try {
-        // Attempt to execute the command
-        Process process = java.lang.Runtime.getRuntime().exec(command);
+    return isExecutableAvailable(command, 3);
+  }
+  
+  /**
+   * Checks if a specific executable command is available in the system.
+   * This function attempts to execute the command with a timeout to ensure
+   * that the check does not hang indefinitely. The process is considered available
+   * if it can be started without throwing an exception and completes successfully
+   * within the specified timeout.
+   * 
+   * @param command the command to check for availability.
+   * @param timeoutSeconds the maximum time in seconds to wait for the command to complete.
+   * @return true if the command is available and completes successfully within the timeout, false otherwise.
+   */
+  public static boolean isExecutableAvailable(String command, int timeoutSeconds) {
+      try {
+          // Attempt to execute the command
+          Process process = java.lang.Runtime.getRuntime().exec(command);
 
-        // Check the exit value of the process
-        // If the process has terminated correctly, the command is available
-        if (process.waitFor() == 0) {
-            return true;
-        }
+          // Wait for the process to complete with a timeout
+          if (process.waitFor(timeoutSeconds, TimeUnit.SECONDS) && process.exitValue() == 0) {
+              return true;
+          }
 
-        // Read any errors from the attempted command
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
-            String line;
-            while ((line = reader.readLine()) != null) {
-                System.out.println(line);
-            }
-        }
+          // Read and log any errors from the attempted command
+          try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+              String line;
+              while ((line = reader.readLine()) != null) {
+                  log.info(line);
+              }
+          }
+
+          return false;
+      } catch (IOException e) {
+          log.info("IOException: " + e.getMessage());
+          return false;
+      } catch (InterruptedException e) {
+          log.info("InterruptedException: " + e.getMessage());
+          // Restore interrupted state
+          Thread.currentThread().interrupt();
+          return false;
+      }
+  }
 
-        return false;
-    } catch (IOException e) {
-        log.info("IOException: " + e.getMessage());
-        return false;
-    } catch (InterruptedException e) {
-        log.info("InterruptedException: " + e.getMessage());
-        return false;
-    }
-}
-  
 
 }