diff --git a/.github/build.yml b/.github/workflows/build.yml similarity index 94% rename from .github/build.yml rename to .github/workflows/build.yml index b510c17054..06d98292ca 100644 --- a/.github/build.yml +++ b/.github/workflows/build.yml @@ -3,8 +3,8 @@ name: Java CI on: - # push: - pull_request: + push: + # pull_request: jobs: build: @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up java uses: actions/setup-java@v3 with: @@ -23,12 +23,12 @@ 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 - name: Build with Maven # currently cannot test opencv - run: mvn clean verify -q + run: mvn clean verify -q -Dtest=!org.myrobotlab.opencv.* - name: Get next version uses: reecetech/version-increment@2023.9.3 @@ -84,4 +84,3 @@ jobs: tag_name: ${{ steps.version.outputs.version }} generate_release_notes: true body_path: ./release-template.md - 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..42be1648fd --- /dev/null +++ b/src/main/java/org/myrobotlab/process/PythonTerminal.java @@ -0,0 +1,106 @@ +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 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..3b5000543b --- /dev/null +++ b/src/main/java/org/myrobotlab/process/Terminal.java @@ -0,0 +1,400 @@ +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 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 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 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; + } + + /** + *
+   *  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.
+  }
+   * 
+ */ + + public Set getPids() { + Set 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 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/programab/Session.java b/src/main/java/org/myrobotlab/programab/Session.java index a234310e57..d7205ba2e4 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; @@ -93,7 +95,7 @@ private synchronized Chat getChat() { return chat; } - public void savePredicates() { + synchronized public void savePredicates() { StringBuilder sb = new StringBuilder(); TreeSet sort = new TreeSet<>(); sort.addAll(getChat().predicates.keySet()); @@ -120,9 +122,14 @@ public void savePredicates() { * Get all current predicate names and values * @return */ - public Map getPredicates() { + synchronized public Map getPredicates() { TreeMap sort = new TreeMap<>(); - sort.putAll(getChat().predicates); + // copy keys, making this sort thread safe + Set 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/AudioFile.java b/src/main/java/org/myrobotlab/service/AudioFile.java index 3997f6960e..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 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 processors = new HashMap(); - double volume = 1.0f; - // if set to true, playback will become a no-op - private boolean mute = false; - - protected String currentPlaylist = "default"; - - protected Map> 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() { @@ -384,6 +379,14 @@ public List getFiles(String subDir, boolean recurse) { return new ArrayList(); } + @Override + public void releaseService() { + super.releaseService(); + for (AudioProcessor processor: processors.values()) { + processor.stopPlaying(); + } + } + public AudioData repeat(String filename) { return repeat(filename, -1); } @@ -433,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 list = null; - if (!playlists.containsKey(name)) { + if (!config.playlists.containsKey(name)) { list = new ArrayList(); } else { - list = playlists.get(name); + list = config.playlists.get(name); } File check = new File(path); if (!check.exists()) { @@ -465,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); } @@ -497,15 +500,15 @@ private List scanForMusicFiles(String path) { } public List getPlaylist(String name) { - return playlists.get(name); + return config.playlists.get(name); } public Map> 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) { @@ -517,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/InMoov2.java b/src/main/java/org/myrobotlab/service/InMoov2.java index 1152be9af6..c971d94147 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2.java +++ b/src/main/java/org/myrobotlab/service/InMoov2.java @@ -5,20 +5,24 @@ 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.LinkedHashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +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; +import org.myrobotlab.framework.Peer; import org.myrobotlab.framework.Plan; 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; @@ -31,11 +35,8 @@ import org.myrobotlab.programab.Session; import org.myrobotlab.service.FiniteStateMachine.StateChange; import org.myrobotlab.service.Log.LogEntry; -import org.myrobotlab.service.abstracts.AbstractSpeechRecognizer; import org.myrobotlab.service.abstracts.AbstractSpeechSynthesis; import org.myrobotlab.service.config.InMoov2Config; -import org.myrobotlab.service.config.OpenCVConfig; -import org.myrobotlab.service.config.SpeechSynthesisConfig; import org.myrobotlab.service.data.JoystickData; import org.myrobotlab.service.data.Locale; import org.myrobotlab.service.interfaces.IKJointAngleListener; @@ -46,21 +47,69 @@ import org.myrobotlab.service.interfaces.Simulator; import org.myrobotlab.service.interfaces.SpeechListener; import org.myrobotlab.service.interfaces.SpeechRecognizer; -import org.myrobotlab.service.interfaces.SpeechSynthesis; import org.myrobotlab.service.interfaces.TextListener; import org.myrobotlab.service.interfaces.TextPublisher; import org.slf4j.Logger; public class InMoov2 extends Service - implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, IKJointAngleListener { + implements ServiceLifeCycleListener, SpeechListener, TextListener, TextPublisher, JoystickListener, LocaleProvider, + IKJointAngleListener { - public final static Logger log = LoggerFactory.getLogger(InMoov2.class); + public class Heart implements Runnable { + private final ReentrantLock lock = new ReentrantLock(); + private Thread thread; - public static LinkedHashMap lpVars = new LinkedHashMap(); + @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; + } + } + } - private static final long serialVersionUID = 1L; + 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; + } + } - static String speechRecognizer = "WebkitSpeechRecognition"; + public final static Logger log = LoggerFactory.getLogger(InMoov2.class); + + private static final long serialVersionUID = 1L; /** * This method will load a python file into the python interpreter. @@ -72,6 +121,7 @@ public class InMoov2 extends Service @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) { @@ -123,89 +173,19 @@ 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); } } - protected transient ProgramAB chatBot; - - protected List configList; - /** - * Configuration from runtime has started. This is when runtime starts - * processing a configuration set for the first time since inmoov was started + * number of times waited in boot state */ - protected boolean configStarted = false; + protected int bootCount = 0; + + protected transient ProgramAB chatBot; - String currentConfigurationName = "default"; + protected List configList; protected transient SpeechRecognizer ear; @@ -217,7 +197,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; @@ -229,9 +209,9 @@ public static void main(String[] args) { */ protected boolean hasBooted = false; - protected long heartbeatCount = 0; + private transient final Heart heart = new Heart(); - protected transient HtmlFilter htmlFilter; + protected long heartbeatCount = 0; protected transient ImageDisplay imageDisplay; @@ -239,8 +219,6 @@ public static void main(String[] args) { protected String lastGestureExecuted; - protected Long lastPirActivityTime; - protected String lastState = null; /** @@ -250,8 +228,6 @@ public static void main(String[] args) { protected int maxInactivityTimeSeconds = 120; - protected transient SpeechSynthesis mouth; - protected boolean mute = false; protected transient OpenCV opencv; @@ -263,10 +239,16 @@ public static void main(String[] args) { */ protected String state = "boot"; + protected long stateLastIdleTime = System.currentTimeMillis(); + + protected long stateLastRandomTime = System.currentTimeMillis(); + protected String voiceSelected; 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 @@ -287,6 +269,8 @@ public InMoov2Config apply(InMoov2Config c) { } else { setLocale(getSupportedLocale(Runtime.getInstance().getLocale().toString())); } + // one way sync configuration into predicates + configToPredicates(); execScript(); @@ -309,7 +293,7 @@ public InMoov2Config apply(InMoov2Config c) { } catch (Exception e) { error(e); - } + } return c; } @@ -345,6 +329,145 @@ public void beginCheckingOnInactivity(int 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 + * 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 + * + *
+       * 
+       * 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);
+       * 
+       * 
+ */ + + // load the InMoov2.py and publish it for Python/Jython or Py4j to consume + execScript(); + + // 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 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 local = Runtime.getLocalServices().values(); + List ordered = new ArrayList<>(local); + ordered.removeIf(Objects::isNull); + Collections.sort(ordered); + + Map peers = getPeers(); + Set 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 cameraOff() { if (opencv != null) { opencv.stopCapture(); @@ -541,15 +664,6 @@ public void displayFullScreen(String src) { } } - public void enable() { - sendToPeer("head", "enable"); - sendToPeer("rightHand", "enable"); - sendToPeer("leftHand", "enable"); - sendToPeer("rightArm", "enable"); - sendToPeer("leftArm", "enable"); - sendToPeer("torso", "enable"); - } - public void enableRandomHead() { Random random = (Random) getPeer("random"); if (random != null) { @@ -560,6 +674,15 @@ public void enableRandomHead() { } } + public void enable() { + sendToPeer("head", "enable"); + sendToPeer("rightHand", "enable"); + sendToPeer("leftHand", "enable"); + sendToPeer("rightArm", "enable"); + sendToPeer("leftArm", "enable"); + sendToPeer("torso", "enable"); + } + /** * Single place for InMoov2 service to execute arbitrary code - needed * initially to set "global" vars in python @@ -619,15 +742,9 @@ public void execScript() { * execute a resource script * @return success or failure */ - public boolean execScript(String someScriptName) { - try { - Python p = (Python) Runtime.start("python", "Python"); - String script = getResourceAsString(someScriptName); - return p.exec(script, true); - } catch (Exception e) { - error("unable to execute script %s", someScriptName); - return false; - } + public void execScript(String someScriptName) { + String script = getResourceAsString(someScriptName); + invoke("publishPython", script); } public void finishedGesture() { @@ -644,9 +761,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() { @@ -693,30 +816,39 @@ public InMoov2Head getHead() { * @return the timestamp of the last activity time. */ public Long getLastActivityTime() { - try { - - Long lastActivityTime = 0L; - - Long head = (Long) sendToPeerBlocking("head", "getLastActivityTime", getName()); - Long leftArm = (Long) sendToPeerBlocking("leftArm", "getLastActivityTime", getName()); - Long rightArm = (Long) sendToPeerBlocking("rightArm", "getLastActivityTime", getName()); - Long leftHand = (Long) sendToPeerBlocking("leftHand", "getLastActivityTime", getName()); - Long rightHand = (Long) sendToPeerBlocking("rightHand", "getLastActivityTime", getName()); - Long torso = (Long) sendToPeerBlocking("torso", "getLastActivityTime", getName()); - - lastActivityTime = Math.max(head, leftArm); - lastActivityTime = Math.max(lastActivityTime, rightArm); - lastActivityTime = Math.max(lastActivityTime, leftHand); - lastActivityTime = Math.max(lastActivityTime, rightHand); - lastActivityTime = Math.max(lastActivityTime, torso); - - return lastActivityTime; - - } catch (Exception e) { - error(e); - return null; + Long head = (InMoov2Head) getPeer("head") != null ? ((InMoov2Head) getPeer("head")).getLastActivityTime() : null; + Long leftArm = (InMoov2Arm) getPeer("leftArm") != null ? ((InMoov2Arm) getPeer("leftArm")).getLastActivityTime() + : null; + Long rightArm = (InMoov2Arm) getPeer("rightArm") != null ? ((InMoov2Arm) getPeer("rightArm")).getLastActivityTime() + : null; + Long leftHand = (InMoov2Hand) getPeer("leftHand") != null + ? ((InMoov2Hand) getPeer("leftHand")).getLastActivityTime() + : null; + Long rightHand = (InMoov2Hand) getPeer("rightHand") != null + ? ((InMoov2Hand) getPeer("rightHand")).getLastActivityTime() + : null; + Long torso = (InMoov2Torso) getPeer("torso") != null ? ((InMoov2Torso) getPeer("torso")).getLastActivityTime() + : null; + + Long lastActivityTime = null; + + if (head != null || leftArm != null || rightArm != null || leftHand != null || rightHand != null || torso != null) { + lastActivityTime = 0L; + if (head != null) + lastActivityTime = Math.max(lastActivityTime, head); + if (leftArm != null) + lastActivityTime = Math.max(lastActivityTime, leftArm); + if (rightArm != null) + lastActivityTime = Math.max(lastActivityTime, rightArm); + if (leftHand != null) + lastActivityTime = Math.max(lastActivityTime, leftHand); + if (rightHand != null) + lastActivityTime = Math.max(lastActivityTime, rightHand); + if (torso != null) + lastActivityTime = Math.max(lastActivityTime, torso); } + return lastActivityTime; } public InMoov2Arm getLeftArm() { @@ -736,24 +868,12 @@ 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) { - ProgramAB chatBot = (ProgramAB) getPeer("chatBot"); - if (chatBot != null) { - return chatBot.getPredicate(user, key); - } else { - error("no chatBot available"); - } - return null; + return chatBot.getPredicate(user, key); } /** @@ -763,14 +883,8 @@ 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; + Response response = chatBot.getResponse(text); + return response; } public InMoov2Arm getRightArm() { @@ -1090,7 +1204,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 log) { errors.addAll(log); @@ -1206,15 +1320,8 @@ public void onPirOff() { */ public void onPirOn() { log.info("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"); - } - } + setPredicate(String.format("%s.pir_on", getName()), System.currentTimeMillis()); + processMessage("onPirOn"); } // GOOD GOOD GOOD - LOOPBACK - flexible and replacable by python @@ -1283,60 +1390,10 @@ public void onStartConfig(String configName) { */ @Override public void onStarted(String name) { - - 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.replace(".", " ")); - - switch (peerKey) { - case "audioPlayer": - break; - case "chatBot": - ProgramAB chatBot = (ProgramAB) Runtime.getService(name); - chatBot.attachTextListener(getPeerName("htmlFilter")); - startPeer("htmlFilter"); - break; - case "ear": - AbstractSpeechRecognizer ear = (AbstractSpeechRecognizer) Runtime.getService(name); - ear.attachTextListener(getPeerName("chatBot")); - break; - case "htmlFilter": - TextPublisher htmlFilter = (TextPublisher) Runtime.getService(name); - htmlFilter.attachTextListener(getPeerName("mouth")); - break; - case "mouth": - mouth = (AbstractSpeechSynthesis) Runtime.getService(name); - mouth.attachSpeechListener(getPeerName("ear")); - break; - case "opencv": - subscribeTo(name, "publishOpenCVData"); - break; - default: - log.info("unknown peer {} not handled 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); @@ -1485,9 +1542,85 @@ public String publishFlash(String flashName) { return flashName; } - public String publishHeartbeat() { - invoke("publishFlash", "heartbeat"); - return getName(); + /** + * 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 + */ + 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 - trying boot"); + 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)) { + // idle event to be handled with the processor + // processMessage("onIdle"); + 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; } /** @@ -1706,6 +1839,7 @@ public void releasePeer(String peerKey) { public void releaseService() { try { disable(); + heart.stop(); super.releaseService(); } catch (Exception e) { error(e); @@ -1878,15 +2012,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; } @@ -1922,8 +2051,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)); @@ -1956,24 +2087,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"); } @@ -2015,105 +2128,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"); } @@ -2129,48 +2143,7 @@ public void startedGesture(String nameOfGesture) { } public void startHeartbeat() { - addTask(1000, "publishHeartbeat"); - } - - // 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 (isVirtual()) { - speakBlocking(get("STARTINGVIRTUALHARD")); - } - speakBlocking(get("WHATISTHISLANGUAGE")); - - return mouth; + heart.start(); } @Deprecated /* use startPeer */ @@ -2191,34 +2164,47 @@ 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(); + // if you hardcode subscriptions here - they should + // be controlled/branched by config + // get service start and release life cycle events runtime.attachServiceLifeCycleListener(getName()); - List services = Runtime.getServices(); - for (ServiceInterface si : services) { - if ("Servo".equals(si.getSimpleName())) { - send(si.getFullName(), "setAutoDisable", true); - } - } - + // 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"); + + if (config.heartbeat) { + startHeartbeat(); + } else { + stopHeartbeat(); + } + } public void startServos() { @@ -2246,12 +2232,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() { @@ -2259,25 +2246,39 @@ public void stopNeopixelAnimation() { } public void systemCheck() { + Platform platform = Runtime.getPlatform(); log.info("systemCheck()"); int servoCount = 0; - int servoAttachedCount = 0; for (ServiceInterface si : Runtime.getServices()) { if (si.getClass().getSimpleName().equals("Servo")) { servoCount++; - if (((Servo) si).getController() != null) { - servoAttachedCount++; - } } } - setPredicate("systemServoCount", servoCount); - setPredicate("systemAttachedServoCount", servoAttachedCount); - setPredicate("systemFreeMemory", Runtime.getFreeMemory()); - Platform platform = Runtime.getPlatform(); - setPredicate("system version", platform.getVersion()); - // ERROR buffer !!! - invoke("publishEvent", "systemCheckFinished"); + // 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.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()); + + } + + 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/InMoov2Arm.java b/src/main/java/org/myrobotlab/service/InMoov2Arm.java index 3b74a3bf20..e676c52878 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; @@ -88,7 +90,7 @@ public static DHRobotArm getDHRobotArm(String name, String side) { return arm; } - + @Deprecated /* use onMove(map) */ public void onMoveArm(HashMap map) { onMove(map); @@ -98,7 +100,6 @@ public void onMove(Map 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 @@ -123,15 +124,6 @@ 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() { @@ -200,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() { @@ -291,17 +283,6 @@ public void onJointAngles(Map 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(); @@ -469,4 +450,26 @@ 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 b30c2bd792..f7fdadebbc 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Hand.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Hand.java @@ -567,20 +567,6 @@ public List 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 f3f5edf366..1e98970f78 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Head.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Head.java @@ -360,12 +360,6 @@ 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 75fa410ca2..efdb127957 100644 --- a/src/main/java/org/myrobotlab/service/InMoov2Torso.java +++ b/src/main/java/org/myrobotlab/service/InMoov2Torso.java @@ -36,27 +36,11 @@ 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(); @@ -93,7 +77,7 @@ public void disable() { if (lowStom != null) lowStom.disable(); } - + @Deprecated /* use onMove(map) */ public void onMoveTorso(HashMap map) { onMove(map); @@ -103,7 +87,6 @@ public void onMove(Map 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()); @@ -111,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/Log.java b/src/main/java/org/myrobotlab/service/Log.java index 16dec730f3..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); @@ -132,6 +138,7 @@ public void addError(String msg) { @Override public void addError(String arg0, Throwable arg1) { + System.out.println("addError"); } @Override @@ -202,6 +209,18 @@ synchronized public void flush() { if (buffer.size() > 0) { // bucket add to sliding window logs.addAll(buffer); + + List errors = new ArrayList<>(); + for(int i = 0; i < buffer.size(); ++i) { + LogEntry entry = buffer.get(i); + if ("ERROR".equals(entry.level)) { + errors.add(entry); + } + } + 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/NeoPixel.java b/src/main/java/org/myrobotlab/service/NeoPixel.java index 21a05cbdc5..a81397f49b 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 @@ -936,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/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/OpenCV.java b/src/main/java/org/myrobotlab/service/OpenCV.java index e050baaf50..2855b3dc50 100644 --- a/src/main/java/org/myrobotlab/service/OpenCV.java +++ b/src/main/java/org/myrobotlab/service/OpenCV.java @@ -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) { @@ -851,7 +846,7 @@ public List 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."; @@ -1134,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); @@ -1161,7 +1152,7 @@ public boolean isRecording() { * 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) { @@ -1312,7 +1303,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) { @@ -1395,7 +1386,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 @@ -1408,7 +1399,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 @@ -1444,7 +1435,7 @@ public void publishNoRecognizedFace() { * until asked for - then its cached SMART ! :) * * @param data - * the opencv data + * the opencv data * @return cvdata * */ @@ -1477,13 +1468,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) { @@ -1495,9 +1486,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) { @@ -1558,7 +1549,7 @@ public void startStreamer() { * key- input, filter, or display * * @param data - * data + * data */ public void record(OpenCVData data) { try { @@ -1580,8 +1571,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"); @@ -1647,7 +1637,7 @@ public ImageData saveImage() { /** * @param name - * remove a filter by name + * remove a filter by name */ @Override synchronized public void removeFilter(String name) { @@ -1729,7 +1719,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) { @@ -1768,15 +1758,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); @@ -1794,9 +1781,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); @@ -1916,8 +1903,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; } } diff --git a/src/main/java/org/myrobotlab/service/ProgramAB.java b/src/main/java/org/myrobotlab/service/ProgramAB.java index 6b0aad8f00..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 @@ -711,12 +718,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 ed601f9044..78213e7552 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 { 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 randomData = new HashMap<>(); + protected Map 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); } @@ -218,7 +218,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(); @@ -237,11 +239,27 @@ public void run() { // and see if any random event needs processing sleep(config.rate); - for (String key : randomData.keySet()) { + + Map tasks = null; + synchronized (lock) { + + // copy to avoid concurrent exceptions, avoid iterating over + // randomData + tasks = new HashMap<>(); + Set keySet = new HashSet(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; } @@ -311,7 +329,7 @@ public RandomConfig getConfig() { super.getConfig(); config.enabled = enabled; - + if (config.randomMessages == null) { config.randomMessages = new HashMap<>(); } @@ -443,15 +461,15 @@ public List methodQuery(String serviceName, String methodName) { } return MethodCache.getInstance().query(si.getClass().getCanonicalName(), methodName); } - - public Map getRandomEvents(){ + + public Map getRandomEvents() { return randomData; } - + public RandomMessage getRandomEvent(String key) { return randomData.get(key); } - + /** * disables all the individual tasks */ @@ -461,7 +479,7 @@ public void disableAll() { } broadcastState(); } - + @Override public void releaseService() { disable(); diff --git a/src/main/java/org/myrobotlab/service/RemoteSpeech.java b/src/main/java/org/myrobotlab/service/RemoteSpeech.java index 343e7e2850..3c41b292ca 100644 --- a/src/main/java/org/myrobotlab/service/RemoteSpeech.java +++ b/src/main/java/org/myrobotlab/service/RemoteSpeech.java @@ -17,10 +17,11 @@ 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 * @@ -35,7 +36,7 @@ public class RemoteSpeech extends AbstractSpeechSynthesis { * HttpClient peer for GETs and POSTs */ public transient HttpClient http = null; - + /** * Currently only support MozillaTTS */ @@ -44,12 +45,12 @@ public class RemoteSpeech extends AbstractSpeechSynthesis { 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/TerminalManager.java b/src/main/java/org/myrobotlab/service/TerminalManager.java new file mode 100644 index 0000000000..69af4548b2 --- /dev/null +++ b/src/main/java/org/myrobotlab/service/TerminalManager.java @@ -0,0 +1,236 @@ +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 { + + 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 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 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/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() { * */ - // 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/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 e public static final String journalFilename = "journal.txt"; - /** - * substitutions are phonetic substitutions for a specific instance of speech - * synthesis service - */ - transient protected Map substitutions = new ConcurrentHashMap(); - /** * generalized list of languages and their codes - if useful */ protected Map locales = new HashMap<>(); - /** - * mute or unmute service - */ - boolean mute = false; - /** * replaces key with replacement */ @@ -252,8 +239,6 @@ public Object getVoiceProvider() { private List 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 parse(String toSpeak) { - + // we generate a list of audio data to play to support // synthesizing this speech List playList = new ArrayList(); - + 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 spokenParts = parseEffects(toSpeak); + List 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 speak(String toSpeak) { @Override public List speakBlocking(String toSpeak) { - boolean prevValue = blocking; - blocking = true; + boolean prevValue = config.blocking; + config.blocking = true; List 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 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 df4a78e446..bcff490640 100644 --- a/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java +++ b/src/main/java/org/myrobotlab/service/config/AudioFileConfig.java @@ -2,18 +2,21 @@ import java.util.List; import java.util.Map; +import java.util.TreeMap; public class AudioFileConfig extends ServiceConfig { public boolean mute = false; - public String currentTrack = "default"; + public double volume = 1.0; - public String currentPlaylist = "default"; - public Map> playlists; - @Deprecated /* use regular "listeners" from ServiceConfig parent */ - public String[] audioListeners; + public String currentPlaylist = "default"; + /** + * Named map of lists of files + */ + public Map> playlists = new TreeMap<>(); + /** * 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 54ee9d764d..b043165cf6 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,8 +244,7 @@ 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"); @@ -266,7 +265,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("-"); @@ -534,7 +533,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 @@ -546,11 +545,10 @@ 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)); - htmlFilter.listeners.add(new Listener("publishText", name)); - htmlFilter.listeners.add(new Listener("publishText", name)); OakDConfig oakd = (OakDConfig) plan.get(getPeerName("oakd")); @@ -564,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 d91131ae66..c1d82dbe29 100644 --- a/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java +++ b/src/main/java/org/myrobotlab/service/config/ProgramABConfig.java @@ -9,7 +9,6 @@ public class ProgramABConfig extends ServiceConfig { @Deprecated /* unused text filters */ public String[] textFilters; - /** * explicit bot directories */ @@ -22,16 +21,15 @@ 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 e79ec273cc..1eca25bdd0 100644 --- a/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java +++ b/src/main/java/org/myrobotlab/service/config/RemoteSpeechConfig.java @@ -10,7 +10,8 @@ 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}"; @@ -18,7 +19,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/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 substitutions; + public String voice; @Override 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 terminals = new TreeMap<>(); + +} 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/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); + + } + +} 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); } diff --git a/src/main/resources/resource/Email.png b/src/main/resources/resource/Email.png new file mode 100644 index 0000000000..595d625b21 Binary files /dev/null and b/src/main/resources/resource/Email.png differ diff --git a/src/main/resources/resource/Lloyd.png b/src/main/resources/resource/Lloyd.png new file mode 100644 index 0000000000..b1af6eab7f Binary files /dev/null and b/src/main/resources/resource/Lloyd.png differ diff --git a/src/main/resources/resource/Maven.png b/src/main/resources/resource/Maven.png new file mode 100644 index 0000000000..355edb4d2c Binary files /dev/null and b/src/main/resources/resource/Maven.png differ diff --git a/src/main/resources/resource/RemoteSpeech.png b/src/main/resources/resource/RemoteSpeech.png new file mode 100644 index 0000000000..450cd2900b Binary files /dev/null and b/src/main/resources/resource/RemoteSpeech.png differ diff --git a/src/main/resources/resource/RoboClaw.png b/src/main/resources/resource/RoboClaw.png new file mode 100644 index 0000000000..a7dbee1a55 Binary files /dev/null and b/src/main/resources/resource/RoboClaw.png differ diff --git a/src/main/resources/resource/Slack.png b/src/main/resources/resource/Slack.png new file mode 100644 index 0000000000..3544ef16f4 Binary files /dev/null and b/src/main/resources/resource/Slack.png differ diff --git a/src/main/resources/resource/TerminalManager.png b/src/main/resources/resource/TerminalManager.png new file mode 100644 index 0000000000..e212bb9b2d Binary files /dev/null and b/src/main/resources/resource/TerminalManager.png differ 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 8820b2de1f..adb479a116 100644 --- a/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js +++ b/src/main/resources/resource/WebGui/app/service/js/RuntimeGui.js @@ -1,5 +1,12 @@ -angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$scope', 'mrl', 'statusSvc', '$timeout', '$uibModal', 'modalService', function($scope, mrl, statusSvc, $timeout, $uibModal, modalService) { - console.info('RuntimeGuiCtrl') +angular.module("mrlapp.service.RuntimeGui", []).controller("RuntimeGuiCtrl", [ + "$scope", + "mrl", + "statusSvc", + "$timeout", + "$uibModal", + "modalService", + function ($scope, mrl, statusSvc, $timeout, $uibModal, modalService) { + console.info("RuntimeGuiCtrl") var _self = this var msg = this.msg @@ -8,13 +15,13 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ // configName is static so it needs to be // kept in sync on a subobject $scope.selected = { - configName :"default" + configName: "default", } - - this.updateState = function(service) { - $scope.service = service - $scope.locale.selected = service.locale.language - $scope.localeTag.selected = service.locale + + this.updateState = function (service) { + $scope.service = service + $scope.locale.selected = service.locale.language + $scope.localeTag.selected = service.locale } $scope.locales = {} @@ -26,28 +33,28 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ $scope.newName = null $scope.newType = "" $scope.heartbeatTs = null - $scope.hosts = [] + $scope.hosts = [] // $scope.selectedOption = "current" $scope.languages = { - 'en': { - 'language': 'en', - 'displayLanguage': 'English' - } + en: { + language: "en", + displayLanguage: "English", + }, } $scope.locale = { - selected: null + selected: null, } $scope.localeTag = { - 'selected': { - 'tag': 'en-US' - } + selected: { + tag: "en-US", + }, } $scope.category = { - selected: null + selected: null, } $scope.categoryServiceTypes = null @@ -61,353 +68,360 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ // $scope.categoryServiceTypes = $scope.service.serviceData.categoryTypes[$scope.category.selected].serviceTypes - $scope.filterServices = function() { - var result = {} - // console.debug('$scope.category.selected is ' + $scope.category.selected) - const entries = Object.entries($scope.service.serviceData.serviceTypes) + $scope.filterServices = function () { + var result = {} + // console.debug('$scope.category.selected is ' + $scope.category.selected) + const entries = Object.entries($scope.service.serviceData.serviceTypes) - if ($scope.category.selected != null && ($scope.category.selected == 'show all')) { - return $scope.service.serviceData.serviceTypes - } + if ($scope.category.selected != null && $scope.category.selected == "show all") { + return $scope.service.serviceData.serviceTypes + } - for (const [fullTypeName,metaData] of entries) { - // if (metaData.simpleName.toLowerCase().includes($scope.newType)) { + for (const [fullTypeName, metaData] of entries) { + // if (metaData.simpleName.toLowerCase().includes($scope.newType)) { - if ($scope.category.selected != null) { - categoryServiceTypes = $scope.service.serviceData.categoryTypes[$scope.category.selected].serviceTypes - } else { - categoryServiceTypes = null - } + if ($scope.category.selected != null) { + categoryServiceTypes = $scope.service.serviceData.categoryTypes[$scope.category.selected].serviceTypes + } else { + categoryServiceTypes = null + } - if (/*metaData.simpleName.toLowerCase().includes($scope.newType) && */ - categoryServiceTypes != null && categoryServiceTypes.includes(metaData.name)) { - result[fullTypeName] = metaData - } + if (/*metaData.simpleName.toLowerCase().includes($scope.newType) && */ categoryServiceTypes != null && categoryServiceTypes.includes(metaData.name)) { + result[fullTypeName] = metaData } - return result + } + return result } // FIXME - maintain contextPath !!! - $scope.sendToCli = function(cmd) { - console.log("sendToCli " + cmd) - $scope.cmd = "" - contextPath = null - msg.send("sendToCli", "runtime@" + mrl.getId(), cmd) + $scope.sendToCli = function (cmd) { + console.log("sendToCli " + cmd) + $scope.cmd = "" + contextPath = null + msg.send("sendToCli", "runtime@" + mrl.getId(), cmd) } - $scope.setServiceType = function(serviceType) { - $scope.newType = serviceType + $scope.setServiceType = function (serviceType) { + $scope.newType = serviceType } - $scope.setConfig = function() { - console.info('setConfig') - if ($scope.selectedConfig.length > 0) { - msg.sendTo('runtime', 'setConfig', $scope.selectedConfig[0]) - msg.sendTo('runtime', 'getConfigName') - } + $scope.setConfig = function () { + console.info("setConfig") + if ($scope.selectedConfig.length > 0) { + msg.sendTo("runtime", "setConfig", $scope.selectedConfig[0]) + msg.sendTo("runtime", "getConfigName") + } } - $scope.start = function() { - - if ($scope.newName == null) { - mrl.error("name of service is required") - return - } - if ($scope.newType == null) { - mrl.error("type of service is required") - return - } - - if (typeof $scope.newType == 'object') { - $scope.newType = $scope.newType.simpleName - } - msg.send('start', $scope.newName, $scope.newType) - - $scope.newName = null - $scope.newType = null + $scope.start = function () { + if ($scope.newName == null) { + mrl.error("name of service is required") + return + } + if ($scope.newType == null) { + mrl.error("type of service is required") + return + } + + if (typeof $scope.newType == "object") { + $scope.newType = $scope.newType.simpleName + } + msg.send("start", $scope.newName, $scope.newType) + + $scope.newName = null + $scope.newType = null } - this.onMsg = function(inMsg) { - let data = null - if (inMsg.data) { - data = inMsg.data[0] - } - - switch (inMsg.method) { - case 'onState': - _self.updateState(data) - $scope.$apply() - break - case 'onPlan': - $scope.plan = data - $scope.$apply() - break - case 'onLocalServices': - $scope.registry = data - // $scope.$apply() - break - case 'onLocale': - $scope.locale.selected = data.language - $scope.$apply() - break - case 'onLocales': - ls = data - unique = {} - $scope.service.locales = {} - // new Set() - for (const key in ls) { - if (ls[key].displayLanguage) { - // unique.add(ls[key].displayLanguage) - // unique.push(ls[key].language) - unique[ls[key].language] = { - 'language': ls[key].language, - 'displayLanguage': ls[key].displayLanguage - } - } - // $scope.service.locales[key] =ls[key] - } - // $scope.languages = Array.from(unique) - $scope.languages = unique - $scope.locales = ls - // it is transient in java to reduce initial registration payload - // $scope.service.locales = ls - $scope.$apply() - break - - case 'onConfigList': - if (data) { - $scope.service.configList = data.sort() - $scope.$apply() - } - break - - case 'onStartYml': - $scope.startYml = data - $scope.$apply() - break - - case 'onSaveDefaults': - if (data.length > 0) { - $scope.defaultsSaved = 'saved defaults to ' + data - msg.send('publishConfigList') - $scope.$apply() - } else { - 'service does not have defaults' + this.onMsg = function (inMsg) { + let data = null + if (inMsg.data) { + data = inMsg.data[0] + } + + switch (inMsg.method) { + case "onState": + _self.updateState(data) + $scope.$apply() + break + case "onPlan": + $scope.plan = data + $scope.$apply() + break + case "onLocalServices": + $scope.registry = data + // $scope.$apply() + break + case "onLocale": + $scope.locale.selected = data.language + $scope.$apply() + break + case "onLocales": + ls = data + unique = {} + $scope.service.locales = {} + // new Set() + for (const key in ls) { + if (ls[key].displayLanguage) { + // unique.add(ls[key].displayLanguage) + // unique.push(ls[key].language) + unique[ls[key].language] = { + language: ls[key].language, + displayLanguage: ls[key].displayLanguage, + } } - break - - case 'onInterfaceToNames': - $scope.interfaceToPossibleServices = data - mrl.interfaceToPossibleServices = data - break - - case 'onServiceTypes': - $scope.serviceTypes = data - mrl.setPossibleServices($scope.serviceTypes) - break - - case 'onRegistered': - console.log("onRegistered") - break - - case 'onConnections': - $scope.connections = data + // $scope.service.locales[key] =ls[key] + } + // $scope.languages = Array.from(unique) + $scope.languages = unique + $scope.locales = ls + // it is transient in java to reduce initial registration payload + // $scope.service.locales = ls + $scope.$apply() + break + + case "onConfigList": + if (data) { + $scope.service.configList = data.sort() $scope.$apply() - break - case 'onHosts': - $scope.hosts = data + } + break + + case "onStartYml": + $scope.startYml = data + $scope.$apply() + break + + case "onSaveDefaults": + if (data.length > 0) { + $scope.defaultsSaved = "saved defaults to " + data + msg.send("publishConfigList") $scope.$apply() - break - case 'onStatus': - $scope.status = data.name + ' ' + data.level + ' ' + data.detail + "\n" + $scope.status + } else { + ;("service does not have defaults") + } + break + + case "onInterfaceToNames": + $scope.interfaceToPossibleServices = data + mrl.interfaceToPossibleServices = data + break + + case "onServiceTypes": + $scope.serviceTypes = data + mrl.setPossibleServices($scope.serviceTypes) + break + + case "onRegistered": + console.log("onRegistered") + break + + case "onConnections": + $scope.connections = data + $scope.$apply() + break + case "onHosts": + $scope.hosts = data + $scope.$apply() + break + case "onStatus": + $scope.status = data.name + " " + data.level + " " + data.detail + "\n" + $scope.status + if ($scope.status.length > 300) { + $scope.status = $scope.status.substring(0, statusMaxSize) + } + break + case "onCli": + if (data != null) { + $scope.status = JSON.stringify(data, null, 2) + "\n" + $scope.status if ($scope.status.length > 300) { - $scope.status = $scope.status.substring(0, statusMaxSize) - } - break - case 'onCli': - if (data != null) { - $scope.status = JSON.stringify(data, null, 2) + "\n" + $scope.status - if ($scope.status.length > 300) { - $scope.status = $scope.status.substring(0, statusMaxSize) - } - $scope.$apply() - } else { - $scope.status += "null\n" + $scope.status = $scope.status.substring(0, statusMaxSize) } - break - case 'onReleased': - console.info("runtime - onRelease " + data) - break - case 'onConfigName': - console.info("runtime - onConfigName " + data) - // is not part of service, because configName is static - $scope.selected.configName = data $scope.$apply() - break - case 'onHeartbeat': - let heartbeat = data - let hb = heartbeat.name + '@' + heartbeat.id + ' sent onHeartbeat - ' - $scope.heartbeatTs = heartbeat.ts - $scope.$apply() - - for (let i in heartbeat.serviceList) { - let serviceName = heartbeat.serviceList[i].name + '@' + heartbeat.serviceList[i].id - hb += serviceName + ' ' - - // FIXME - 'merge' ie remove missing services - - // FIXME - want to maintain "local" registry ??? - // currently maintaining JS process registry - should the RuntimeGui also maintain - // its 'own' sub-registry ??? - if (!serviceName in mrl.getRegistry()) { - // - console.warn(serviceName + ' not defined in registry - sending registration request') - } - // else already registered + } else { + $scope.status += "null\n" + } + break + case "onReleased": + console.info("runtime - onRelease " + data) + break + case "onConfigName": + console.info("runtime - onConfigName " + data) + // is not part of service, because configName is static + $scope.selected.configName = data + $scope.$apply() + break + case "onHeartbeat": + let heartbeat = data + let hb = heartbeat.name + "@" + heartbeat.id + " sent onHeartbeat - " + $scope.heartbeatTs = heartbeat.ts + $scope.$apply() + + for (let i in heartbeat.serviceList) { + let serviceName = heartbeat.serviceList[i].name + "@" + heartbeat.serviceList[i].id + hb += serviceName + " " + + // FIXME - 'merge' ie remove missing services + + // FIXME - want to maintain "local" registry ??? + // currently maintaining JS process registry - should the RuntimeGui also maintain + // its 'own' sub-registry ??? + if (!serviceName in mrl.getRegistry()) { + // + console.warn(serviceName + " not defined in registry - sending registration request") } + // else already registered + } - console.info(hb) + console.info(hb) - // CHECK REGISTRY - // SYNC SERVICES - // REQUEST REGISTRATIONS !!!! - break + // CHECK REGISTRY + // SYNC SERVICES + // REQUEST REGISTRATIONS !!!! + break default: - console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) - break - } + console.error("ERROR - unhandled method " + $scope.name + " " + inMsg.method) + break + } } - $scope.shutdown = function(type) { - var modalInstance = $uibModal.open({ - //animation: true, - // templateUrl: 'nav/shutdown.html', - // template: '', - // controller: $scope.doShutdown, - // controller: 'RuntimeGuiCtrl', - scope: $scope, - // controller: 'ModalController', - - animation: true, - templateUrl: 'nav/shutdown.html', - controller: 'shutdownCtrl2', - - resolve: { - type: function() { - return type - } - } - }) - console.info('shutdown ' + modalInstance) + $scope.shutdown = function (type) { + var modalInstance = $uibModal.open({ + //animation: true, + // templateUrl: 'nav/shutdown.html', + // template: '', + // controller: $scope.doShutdown, + // controller: 'RuntimeGuiCtrl', + scope: $scope, + // controller: 'ModalController', + + animation: true, + templateUrl: "nav/shutdown.html", + controller: "shutdownCtrl2", + + resolve: { + type: function () { + return type + }, + }, + }) + console.info("shutdown " + modalInstance) } - $scope.setAllLocales = function(locale) { - console.info(locale) + $scope.setAllLocales = function (locale) { + console.info(locale) } - $scope.loadConfig = function() { - console.info('loadConfig') - if ($scope.selectedConfig.length) { - for (let i = 0; i < $scope.selectedConfig.length; ++i) { - // msg.sendTo('runtime', 'load', 'data/config/' + $scope.selectedConfig[i] + '/runtime.yml') - msg.sendTo('runtime', 'setConfig', $scope.selectedConfig[i]) - msg.sendTo('runtime', 'load', 'runtime') - } + $scope.loadConfig = function () { + console.info("loadConfig") + if ($scope.selectedConfig.length) { + for (let i = 0; i < $scope.selectedConfig.length; ++i) { + // msg.sendTo('runtime', 'load', 'data/config/' + $scope.selectedConfig[i] + '/runtime.yml') + msg.sendTo("runtime", "setConfig", $scope.selectedConfig[i]) + msg.sendTo("runtime", "load", "runtime") } + } } - $scope.unsetConfig = function() { - console.info('unsetConfig') - msg.sendTo('runtime', 'unsetConfig') + $scope.unsetConfig = function () { + console.info("unsetConfig") + msg.sendTo("runtime", "unsetConfig") } - $scope.startConfig = function() { - console.info('startConfig') - if ($scope.selectedConfig.length) { - for (let i = 0; i < $scope.selectedConfig.length; ++i) { - // msg.sendTo('runtime', 'load', 'data/config/' + $scope.selectedConfig[i] + '/runtime.yml') - msg.sendTo('runtime', 'startConfig', $scope.selectedConfig[i]) - } + $scope.startConfig = function () { + console.info("startConfig") + if ($scope.selectedConfig.length) { + for (let i = 0; i < $scope.selectedConfig.length; ++i) { + // msg.sendTo('runtime', 'load', 'data/config/' + $scope.selectedConfig[i] + '/runtime.yml') + msg.sendTo("runtime", "startConfig", $scope.selectedConfig[i]) } + } } - $scope.releaseConfig = function() { - console.info('releaseConfig') - if ($scope.selectedConfig && $scope.selectedConfig.length) { - for (let i = 0; i < $scope.selectedConfig.length; ++i) { - msg.sendTo('runtime', 'releaseConfig', $scope.selectedConfig[i]) - // msg.sendTo('runtime', 'releaseConfig', 'runtime') - } - } else { - msg.sendTo('runtime', 'releaseConfig') + $scope.releaseConfig = function () { + console.info("releaseConfig") + if ($scope.selectedConfig && $scope.selectedConfig.length) { + for (let i = 0; i < $scope.selectedConfig.length; ++i) { + msg.sendTo("runtime", "releaseConfig", $scope.selectedConfig[i]) + // msg.sendTo('runtime', 'releaseConfig', 'runtime') } + } else { + msg.sendTo("runtime", "releaseConfig") + } } - $scope.savePlan = function() { - console.info('saveConfig') - - let onOK = function() { - msg.sendTo('runtime', 'savePlan', $scope.selected.configName) - // msg.sendTo('runtime', 'save') - } - - let onCancel = function() { - console.info('save config cancelled') - } - - let ret = modalService.openOkCancel('widget/modal-dialog.view.html', 'Save Plan Configuration', 'Save your current configuration in a directory named', onOK, onCancel, $scope) - console.info('ret ' + ret) + $scope.savePlan = function () { + console.info("saveConfig") + + let onOK = function () { + msg.sendTo("runtime", "savePlan", $scope.selected.configName) + // msg.sendTo('runtime', 'save') + } + + let onCancel = function () { + console.info("save config cancelled") + } + + let ret = modalService.openOkCancel( + "widget/modal-dialog.view.html", + "Save Plan Configuration", + "Save your current configuration in a directory named", + onOK, + onCancel, + $scope + ) + console.info("ret " + ret) } - $scope.saveDefaults = function() { - console.info('saveDefaults') - msg.send('saveDefaults', $scope.newType.simpleName) + $scope.saveDefaults = function () { + console.info("saveDefaults") + msg.send("saveDefaults", $scope.newType.simpleName) } - $scope.setAutoStart = function(b) { - console.info('setAutoStart') - msg.send('setAutoStart', b) + $scope.setAutoStart = function (b) { + console.info("setAutoStart") + msg.send("setAutoStart", b) } - $scope.saveConfig = function() { - $scope.service.includePeers = false - $scope.service.selectedOption = "current" - - $scope.selected.configName = $scope.selected.configName - var modalInstance = $uibModal.open({ - templateUrl: 'saveConfig.html', - scope: $scope, - controller: function($scope, $uibModalInstance) { - - $scope.ok = function() { - $uibModalInstance.close() - } - - $scope.cancel = function() { - $uibModalInstance.dismiss('cancel') - } - } - }) - - modalInstance.result.then(function(result) { - // Handle 'OK' button click - console.log('Config Name: ' + $scope.selected.configName) - console.log('Selected Option: ' + $scope.service.selectedOption) - console.log('includePeers Option: ' + $scope.service.includePeers) - console.log('configType Option: ' + $scope.service.configType) - msg.send('setConfig', $scope.selected.configName) - if ($scope.service.selectedOption == 'default'){ - msg.send('saveDefault', $scope.selected.configName, $scope.service.defaultServiceName, $scope.service.configType, $scope.service.includePeers) - } else { - msg.sendTo('runtime', 'saveConfig', $scope.selected.configName) - } - msg.send('getConfigName') - }, function() { - // Handle 'Cancel' button click or modal dismissal - console.log('Modal dismissed') - }) + $scope.saveConfig = function () { + $scope.service.includePeers = false + $scope.service.selectedOption = "current" + + $scope.selected.configName = $scope.selected.configName + var modalInstance = $uibModal.open({ + templateUrl: "saveConfig.html", + scope: $scope, + controller: function ($scope, $uibModalInstance) { + $scope.ok = function () { + $uibModalInstance.close() + } + + $scope.cancel = function () { + $uibModalInstance.dismiss("cancel") + } + }, + }) + + modalInstance.result.then( + function (result) { + // Handle 'OK' button click + console.log("Config Name: " + $scope.selected.configName) + console.log("Selected Option: " + $scope.service.selectedOption) + console.log("includePeers Option: " + $scope.service.includePeers) + console.log("configType Option: " + $scope.service.configType) + msg.send("setConfig", $scope.selected.configName) + if ($scope.service.selectedOption == "default") { + msg.send("saveDefault", $scope.selected.configName, $scope.service.defaultServiceName, $scope.service.configType, $scope.service.includePeers) + } else { + msg.sendTo("runtime", "saveConfig", $scope.selected.configName) + } + msg.send("getConfigName") + }, + function () { + // Handle 'Cancel' button click or modal dismissal + console.log("Modal dismissed") } - + ) + } + // $scope.serviceTypes = Object.values(mrl.getPossibleServices()) msg.subscribe("getStartYml") msg.subscribe("saveDefaults") @@ -420,8 +434,8 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ msg.subscribe("getLocales") msg.subscribe("getHosts") msg.subscribe("publishStatus") - msg.subscribe('publishConfigList') - msg.subscribe('publishInterfaceToNames') + msg.subscribe("publishConfigList") + msg.subscribe("publishInterfaceToNames") // msg.subscribe("getPlan") //msg.send("getLocalServices") @@ -436,5 +450,5 @@ angular.module('mrlapp.service.RuntimeGui', []).controller('RuntimeGuiCtrl', ['$ // msg.send("getHosts") msg.subscribe(this) -} + }, ]) 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..17a85e6e53 --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/js/TerminalManagerGui.js @@ -0,0 +1,72 @@ +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
 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/tab-header.html b/src/main/resources/resource/WebGui/app/service/tab-header.html
index 19434b498d..792051d6bf 100644
--- a/src/main/resources/resource/WebGui/app/service/tab-header.html
+++ b/src/main/resources/resource/WebGui/app/service/tab-header.html
@@ -52,7 +52,7 @@
             
             
  • - +   subscriptions 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..fe42096ccb --- /dev/null +++ b/src/main/resources/resource/WebGui/app/service/views/TerminalManagerGui.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    NamePIDShellCommandControl
    + + + + + Connection Status + + {{key}} + + {{value.pid}} + + {{value.shellCommand}} + + {{value.lastInput}} + + + + + + + +
    + +
    + {{key}} + +
    {{value.output}}
    + + +
    \ No newline at end of file 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 @@
    - +
    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(); diff --git a/src/test/java/org/myrobotlab/service/RandomTest.java b/src/test/java/org/myrobotlab/service/RandomTest.java index f089f2e453..1950a4476e 100644 --- a/src/test/java/org/myrobotlab/service/RandomTest.java +++ b/src/test/java/org/myrobotlab/service/RandomTest.java @@ -40,8 +40,10 @@ public void testService() throws Exception { 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"); @@ -70,8 +72,10 @@ public void testService() throws Exception { 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 => 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(); @@ -85,8 +89,10 @@ public void testService() throws Exception { 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(); 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 servicesNotInServiceDataJson = new ArrayList(); HashSet blacklist = new HashSet(); - 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); diff --git a/src/test/java/org/myrobotlab/test/AbstractTest.java b/src/test/java/org/myrobotlab/test/AbstractTest.java index 7279a8fe98..b21d90c6c1 100644 --- a/src/test/java/org/myrobotlab/test/AbstractTest.java +++ b/src/test/java/org/myrobotlab/test/AbstractTest.java @@ -4,12 +4,9 @@ import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Queue; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.LinkedBlockingQueue; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -17,7 +14,6 @@ import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.myrobotlab.codec.CodecUtils; -import org.myrobotlab.framework.interfaces.Attachable; import org.myrobotlab.logging.LoggerFactory; import org.myrobotlab.service.Runtime; import org.myrobotlab.service.config.RuntimeConfig; @@ -31,9 +27,9 @@ public class AbstractTest { 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 + * 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; @@ -167,31 +163,31 @@ 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); // wait for draining threads sleep(100); - // resets runtime with fresh new instance - Runtime.getInstance(); // check threads - kill stragglers // Set stragglers = new HashSet(); Set threadSetEnd = Thread.getAllStackTraces().keySet(); Set threadsRemaining = new TreeSet<>(); for (Thread thread : threadSetEnd) { - if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName()) && !"runtime".equals(thread.getName())) { - threadsRemaining.add(thread.getName()); + if (!threadSetStart.contains(thread) && !"runtime_outbox_0".equals(thread.getName()) + && !"runtime".equals(thread.getName())) { + threadsRemaining.add(thread.getName()); } } if (threadsRemaining.size() > 0) { 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() {